我希望尽可能少使用正式定义和简单的数学语言。
我希望尽可能少使用正式定义和简单的数学语言。
快速提醒一下,我的答案几乎肯定会将“大O符号”(上界)与大Theta符号“Θ”(双侧界)混淆。但据我经验,在非学术场合讨论时这实际上是很常见的。为任何造成的困惑道歉。
这张图可以帮助你直观地理解BigOh复杂度:
我能给出的最简单的Big Oh符号定义是:我想快速提到的另一点是,任何复杂度为O(na)的算法都被称为具有多项式复杂度或可以在多项式时间内解决。
O(n)、O(n2)等都属于多项式时间。有些问题无法在多项式时间内解决。因此,世界上使用了一些特殊的东西,公钥加密就是一个典型例子。计算非常大数字的两个质因数是计算上很难的。如果不是这样,我们就不能使用目前的公钥系统。
总之,这就是我对BigOh(修订版)的(希望是通俗易懂的)解释。
这段内容展示了算法如何根据输入大小进行伸缩。
O(n2),也称二次复杂度
请注意,项目数量增加了10倍,但时间增加了100倍。基本上,当n=10时,O(n2)给我们的是伸缩因子n2,即102。
O(n),也称线性复杂度
这一次,项目数量增加了10倍,所花费的时间也增加了10倍。当n=10时,O(n)的伸缩因子为10。
O(1),也称常数复杂度
项目数量仍然以10倍的速度增加,但O(1)的伸缩因子始终为1。
O(log n),也称对数复杂度
计算量仅按输入值的对数增加。因此,在这种情况下,假设每个计算需要1秒钟,输入n的对数就是所需的时间,因此为log n
。
这就是要点。他们简化了数学公式,因此可能不会完全是n2或其他他们所说的那样,但这将成为扩展中占主导地位的因素。
大 O 符号(也称为“渐进增长”符号)是指在忽略常数因素和接近原点的内容时,函数的“外观”。我们使用它来描述事物的规模如何变化。
基础知识
对于“足够”大的输入...
f(x) ∈ O(upperbound)
表示 f
的增长速度不超过 upperbound
f(x) ∈ Ɵ(justlikethis)
表示 f
的增长速度与 justlikethis
相同f(x) ∈ Ω(lowerbound)
表示 f
的增长速度不低于 lowerbound
大O符号并不关心常数因子:函数 9x²
被认为“与”10x²
的增长速度相同。大O的渐进符号也不关心非渐进部分(“接近原点的部分”或“问题规模较小时发生的事情”):函数10x²
被认为“与”10x²-x+2
的增长速度相同。
为什么要忽略方程中的较小部分?因为随着你考虑越来越大的规模,它们被方程中的大部分完全淹没;它们的贡献变得微不足道和无关紧要。(请参见示例部分。)
换句话说,随着输入趋近于无限大,一切都与比率有关。 如果你将实际所需时间除以O(...)
,在输入足够大的极限情况下,你会得到一个固定因子。 直观来说这很合理:如果能够通过乘以一个函数来获得另一个函数,那么这两个函数就是"相似的"。 这也是我们说...actualAlgorithmTime(N) ∈ O(bound(N))
e.g. "time to mergesort N elements
is O(N log(N))"
actualAlgorithmTime(N) e.g. "mergesort_duration(N) "
────────────────────── < constant ───────────────────── < 2.5
bound(N) N log(N)
应用
作为纯数学概念,大O符号不仅局限于处理时间和内存方面的讨论。可以用它来讨论任何具有可比性的缩放问题,例如:
N
个人之间可能握手的次数(Ɵ(N²)
,具体来说是N(N-1)/2
,但重要的是它“缩放”像N²
一样)例子
对于上面的握手例子,房间里的每个人都与其他人握手。在这个例子中,#handshakes ∈ Ɵ(N²)
。为什么呢?
回到一开始:握手的数量正好是n-choose-2或N*(N-1)/2
(每个N个人都要与其他N-1个人握手,但这会重复计算握手次数,所以要除以2):
然而,对于非常多的人数,线性项N
被忽略了,实际上对比例没有贡献(在图表中:对角线上空格子的比例随着参与者数量的增加而变小)。因此,缩放行为是order N²
,或者握手次数“像N²一样增长”。
#handshakes(N)
────────────── ≈ 1/2
N²
我就好像是图表对角线上的空白格子(N*(N-1)/2个勾)不存在一样,(当N趋向于无穷大时,会有N2个勾)。
(从“普通英语”暂时偏离一下:)如果你想要自己证明这个结论,你可以对比率进行一些简单的代数运算,将其分解为多个项(lim
表示“在极限情况下考虑”,如果您没有见过它,请忽略它,它只是“当N非常非常大时”的符号表示):
N²/2 - N/2 (N²)/2 N/2 1/2
lim ────────── = lim ( ────── - ─── ) = lim ─── = 1/2
N→∞ N² N→∞ N² N² N→∞ 1
┕━━━┙
this is 0 in the limit of N→∞:
graph it, or plug in a really large number for N
tl;dr:当握手次数很大时,“握手次数”看起来非常像x²,因此,如果我们写下比率#handshakes/x²,即使我们不需要恰好x²次握手,在任意大的时间内,这个事实也不会显示在小数中。
例如,对于x = 100万,比率#handshakes/x²为0.499999 ...
建立直觉
这使我们可以做出像这样的陈述...
“对于足够大的输入大小N,无论常数因子是什么,如果我加倍输入大小...
N → (2N) = 2(N)
N² → (2N)² = 4(N²)
cN³ → c(2N)³ = 8(cN³)
c log(N) → c log(2N) = (c log(2))+(c log(N)) = (固定数量)+(c log(N))
c*1 → c*1
c 2N log(2N) / c N log(N) (这里我们将f(2n)/f(n)除以,但我们可以像上面那样操纵表达式并分解出cNlogN)
→ 2 log(2N)/log(N)
→ 2 (log(2) + log(N))/log(N)
→ 2*(1+(log2N)-1) (对于大N基本上为2; 最终小于2.000001)
(或者,说log(N)将始终低于你的数据的17,因此它是线性的;尽管这不严谨也不合理)
2N → 22N = (4N)............换句话说......2N → 2N+1 = 2N21 = 22N
[对于数学倾向的人,您可以将鼠标悬停在剧透上以获取次要说明]
(感谢https://dev59.com/-HRB5IYBdhLWcg3w3K0J#487292)
(从技术上讲,在某些更为深奥的示例中,常数因素可能很重要,但是我已经用上面的方式(例如在log(N)中)表述了这些内容,使其不重要)
这些是程序员和应用计算机科学家用作参考点的基本增长顺序。他们经常看到这些。 (因此,虽然您可以从技术上认为“将输入加倍会使O(√N)算法变慢1.414倍”,但最好认为它“比对数更差,但比线性更好”。)
常数因子
通常情况下,我们不关心特定的常数因子,因为它们不会影响函数增长的方式。例如,两个算法可能都需要O(N)
时间才能完成,但其中一个可能比另一个慢两倍。除非因子非常大,否则我们通常不会太在意,因为优化是棘手的问题(什么时候进行过度优化?);此外,仅仅选择一个具有更好的大O算法通常就能将性能提高几个数量级。
一些渐进上优的算法(例如非比较的O(Nlog(log(N)))
排序)可能具有如此巨大的常数因子(例如100000*N log(log(N))
),或者相对较大的开销,例如隐含的+ 100*N
的 O(Nlog(log(N)))
,即使在“大数据”上使用也很少值得使用。
为什么O(N)有时是你所能做到的最好算法,即为什么我们需要数据结构
如果您需要读取所有数据,则O(N)
算法在某种意义上是“最佳”算法。阅读一堆数据的行为本身就是一个O(N)
操作。将其加载到内存中通常是O(N)
(如果您具有硬件支持,则更快,或者如果您已经读取了数据,则根本不需要时间)。但是,如果您触摸甚至查看每个数据片段(甚至是每个其他数据片段),则您的算法将花费O(N)
时间来执行此查看。无论实际算法花费多长时间,它都至少需要O(N)
,因为它花费了时间查看所有数据。
对于写入行为本身也可以说同样的事情。所有打印出N个东西的算法都需要N的时间,因为输出至少那么长(例如,打印出N张纸牌的所有排列方式是阶乘:O(N!)
(这就是为什么在这些情况下,好的程序将确保迭代使用O(1)
内存并且不会打印或存储每个中间步骤)。
O(N)
时间),加上一些任意数量的预处理(例如O(N)
或O(N log(N))
或O(N²)
),我们尝试保持尽可能小。此后,对数据结构进行修改(插入/删除/等)和对数据进行查询所需的时间非常短,例如O(1)
或O(log(N))
。然后,您可以继续进行大量的查询!一般来说,您愿意提前做的工作越多,以后就要做的工作就越少。
例如,假设您拥有数百万个道路段的纬度和经度坐标,并希望找到所有街道交叉口。
O(N)
工作不是问题,但如果您要执行多次(在本例中,每个段都需要执行一次),我们将需要执行 O(N²)
的工作量,即1000000² = 1000000000000次操作。这不好(现代计算机每秒可以执行约十亿次操作)。O(N)
的时间预处理所有内容来支付一定的成本。此后,平均情况下只需恒定时间就可以通过键查找某些内容(在本例中,我们的键是经纬度坐标,四舍五入到网格中;我们搜索仅有9个相邻网格空间,这是一个常数)。O(N²)
变为可管理的O(N)
,而我们所做的只是支付了少量成本以创建哈希表。实际例子:编写代码时可视化增长量
渐进符号是一个数学框架,用于思考事物的规模,可以应用于许多不同的领域,与编程本质上是相互独立的。尽管如此...这就是您如何将渐进符号应用于编程。
基础知识:每当我们与大小为 A 的集合中的每个元素进行交互(例如,数组、集合、映射的所有键等),或执行 A 次循环迭代时,这都是大小 A 的乘性因子。为什么我说“乘性因子”?--因为循环和函数(几乎是定义)具有乘性运行时间:迭代次数乘以循环内部的工作量(或者对于函数而言:调用函数的次数乘以函数内部的工作量)。 (如果我们不做任何花哨的事情,比如跳过循环或提前退出循环,或者根据参数改变函数的控制流,这非常常见。)以下是一些可视化技术示例,附带伪代码。
(在这里,“x”表示常量时间单位的工作量,处理器指令,解释器操作码,或其他类似内容)
for(i=0; i<A; i++) // A * ...
some O(1) operation // 1
--> A*1 --> O(A) time
visualization:
|<------ A ------->|
1 2 3 4 5 x x ... x
other languages, multiplying orders of growth:
javascript, O(A) time and space
someListOfSizeA.map((x,i) => [x,i])
python, O(rows*cols) time and space
[[r*c for c in range(cols)] for r in range(rows)]
例子2:
for every x in listOfSizeA: // A * (...
some O(1) operation // 1
some O(B) operation // B
for every y in listOfSizeC: // C * (...
some O(1) operation // 1))
--> O(A*(1 + B + C))
O(A*(B+C)) (1 is dwarfed)
visualization:
|<------ A ------->|
1 x x x x x x ... x
2 x x x x x x ... x ^
3 x x x x x x ... x |
4 x x x x x x ... x |
5 x x x x x x ... x B <-- A*B
x x x x x x x ... x |
................... |
x x x x x x x ... x v
x x x x x x x ... x ^
x x x x x x x ... x |
x x x x x x x ... x |
x x x x x x x ... x C <-- A*C
x x x x x x x ... x |
................... |
x x x x x x x ... x v
例子3:
function nSquaredFunction(n) {
total = 0
for i in 1..n: // N *
for j in 1..n: // N *
total += i*k // 1
return total
}
// O(n^2)
function nCubedFunction(a) {
for i in 1..n: // A *
print(nSquaredFunction(a)) // A^2
}
// O(a^3)
如果我们做一些稍微复杂的事情,你可能仍然能够在视觉上想象出正在发生的事情:
for x in range(A):
for y in range(1..x):
simpleOperation(x*y)
x x x x x x x x x x |
x x x x x x x x x |
x x x x x x x x |
x x x x x x x |
x x x x x x |
x x x x x |
x x x x |
x x x |
x x |
x___________________|
这里,你能画出的最小可识别轮廓是有意义的;三角形是二维形状(0.5 A^2),就像正方形是二维形状(A^2)一样;这里的两个常数因子在它们之间的渐近比率中保持不变,但是,我们像所有因素一样忽略它...(这种技术存在一些不幸的细微差别,我不在这里详细说明;它可能会误导你。)
当然,这并不意味着循环和函数是不好的;相反,它们是现代编程语言的构建块,我们喜欢它们。然而,我们可以看到,我们将循环、函数和条件与我们的数据(控制流等)编织在一起的方式模仿了我们程序的时间和空间使用!如果时间和空间使用成为问题,那么我们就会寻求巧妙之处,并找到一种简单的算法或数据结构,以某种方式减少增长次序。尽管这些可视化技术(虽然它们并不总是有效)可能会给出一个最坏情况运行时间的天真猜测。
这里还有另一件事情,我们可以从视觉上识别:
<----------------------------- N ----------------------------->
x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x
x x x x x x x x x x x x x x x x
x x x x x x x x
x x x x
x x
x
我们可以重新排列这个公式来看出它的时间复杂度为O(N):
<----------------------------- N ----------------------------->
x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x
x x x x x x x x x x x x x x x x|x x x x x x x x|x x x x|x x|x
或者你可以对数据进行log(N)次操作,总时间复杂度为O(N*log(N)):
<----------------------------- N ----------------------------->
^ x x x x x x x x x x x x x x x x|x x x x x x x x x x x x x x x x
| x x x x x x x x|x x x x x x x x|x x x x x x x x|x x x x x x x x
lgN x x x x|x x x x|x x x x|x x x x|x x x x|x x x x|x x x x|x x x x
| x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x
v x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x
不相关但值得再次提到的是:如果我们执行哈希(例如字典/哈希表查找),那么时间复杂度为O(1)。这相当快。
[myDictionary.has(x) for x in listOfSizeA]
\----- O(1) ------/
--> A*1 --> O(A)
摊销和平均情况复杂度
还有"摊销"和/或"平均情况"的概念(请注意这些是不同的)。
平均情况:这只是使用大O符号来表示函数的期望值,而不是函数本身。在通常情况下,您认为所有输入都是等可能的,平均情况只是运行时间的平均值。例如,在快速排序中,即使最坏情况是对于一些非常糟糕的输入O(N^2)
,平均情况仍然是通常的O(N log(N))
(非常糟糕的输入数量很少,我们在平均情况下几乎不会注意到它们)。
O(1)
时间。然而,偶尔它会“打嗝”,并且需要O(N)
时间进行一次随机操作,因为可能需要进行一些簿记或垃圾回收等操作...但是它向您承诺,如果它打嗝了,它不会再打嗝了N次操作。每个操作的最坏情况成本仍然是O(N)
,但在多次运行中的摊销成本为O(N)/N
=O(1)
每个操作。由于大型操作非常罕见,因此偶尔的大量工作可以被视为与其余工作混合在一起的常数因子。我们说工作在足够多次调用中“摊销”后,在渐近意义下消失。
摊销分析的类比:
你开车。偶尔,您需要花费10分钟去加油站,然后花费1分钟加满油箱。如果您每次开车都这样做(花费10分钟驾车到加油站,花费几秒钟加满一加仑),那将非常低效。但是,如果您每隔几天就加满一次油箱,则花费11分钟驾车到加油站就会“摊销”在足够多的行程中,您可以忽略它,并假装所有行程可能会长5%。
平均情况和摊销最坏情况的比较:
虽然,如果您合理担心攻击者,除了摊销和平均情况之外,还有许多其他算法攻击向量需要担心。
平均情况和摊销都是设计时考虑可扩展性非常有用的工具。
(如果对此子主题感兴趣,请参见平均情况和摊销分析之间的区别。)
多维大O表示法
大多数情况下,人们没有意识到有多个变量在起作用。例如,在字符串搜索算法中,您的算法可能需要时间 O([文本长度]+[查询长度])
,即它是两个变量的线性函数,如 O(N+M)
。其他更为天真的算法可能是 O([文本长度]*[查询长度])
或 O(N*M)
。忽略多个变量是我在算法分析中看到的最常见的疏忽之一,这可能会妨碍您设计算法。
整个故事
请记住,大O并不是整个故事。您可以通过使用缓存、使它们成为缓存无关的算法、通过使用RAM而不是磁盘来避免瓶颈、使用并行化或提前完成工作等技术来大大加快某些算法的速度 - 尽管这些技术通常与生长阶数“大O”符号无关,但您经常会在并行算法的大O符号中看到核心数量。
还要记住,由于程序的隐藏约束条件,您可能真的不关心渐近行为。例如,您可能正在处理有限数量的值:
O(N log(N))
快速排序;您希望使用插入排序,在小输入上表现良好。这些情况经常在分治算法中出现,其中将问题分解为越来越小的子问题,例如递归排序、快速傅里叶变换或矩阵乘法。实际上,即使在具有相同或类似渐近性能的算法中,它们的相对优点实际上可能受到其他因素的驱动,例如:其他性能因素(快速排序和归并排序都是O(N log(N))
,但快速排序利用了CPU缓存);非性能考虑因素,如易于实现;是否可用库以及库的声誉和维护情况。
O(log(N))
而不是O(1)
用于插入或查找最小值);你会使用另一种实现吗?可能不会,因为C实现可能更快,而且其他地方可能也存在类似的问题。存在权衡;有时它们很重要,有时它们不重要。
(编辑: 这里的“简明英语”解释到此为止。)
数学附录
为了完整起见,大O符号的精确定义如下: f(x) ∈ O(g(x))
意味着"f在常数*g的渐进上界之内":忽略x的某个有限值以下的一切,存在一个常数,使得 |f(x)| ≤ const * |g(x)|
。(其他符号如下: 就像O
表示≤,Ω
表示≥。有小写变体: o
表示<,ω
表示>.) f(x) ∈ Ɵ(g(x))
意味着既有 f(x) ∈ O(g(x))
又有 f(x) ∈ Ω(g(x))
(上下由g界定): 存在某些常数,使得f将始终处于const1*g(x)
和const2*g(x)
之间的“带状区域”中。这是你可以做出的最强渐近语句,大致相当于==
。(对不起,我决定推迟绝对值符号的提及,为了清晰起见;特别是因为我从未见过在计算机科学上下文中出现负值。)
= O(...)
,这可能是更正确的“计算机科学”符号,并且完全可以使用;"f = O(...)"被读作"f is order ... / f is xxx-bounded by ...",并被认为是"f is some expression whose asymptotics are ..."。我被教导使用更严格的∈ O(...)
。∈
意味着"is an element of"(仍然如前所述)。在这种特殊情况下,O(N²)
包含像{2 N²
,3 N²
,1/2 N²
,2 N² + log(N)
,- N² + N^1.9
,...}这样的元素,它是无限大的,但它仍然是一个集合。 O(...)
当他们想表达 Ɵ(...)
时; 这在技术上是正确的,因为 Ɵ(exactlyThis)
的集合是 O(noGreaterThanThis)
的子集...并且更容易输入。;-)< / p>
编辑说明:快速提醒,这几乎肯定会混淆大O符号(它是一个上限)和Theta符号(它是上限和下限)。 根据我的经验,这在非学术环境中的讨论中实际上是很典型的。对于造成的任何困惑,请接受道歉。
简而言之:随着工作规模的增加,完成工作需要多长时间?
显然,这仅使用“大小”作为输入和“花费的时间”作为输出-如果您想谈论内存使用等,则适用相同的思想。
这里是一个例子,我们有N件T恤需要晾干。 我们将假设将它们放在晾衣架上的速度非常快(即人类互动可忽略不计)。 当然,在现实生活中并非如此...
使用室外晾衣绳:假设您拥有一个无限大的后院,洗涤物在O(1)时间内变干。不管你有多少衣服,它们都会得到同样的阳光和新鲜空气,所以大小不影响干燥时间。
使用烘干机:每次可以放10件衬衫,然后它们在一小时后就完成了。(忽略实际数字——它们是无关紧要的。)因此,干燥50件衬衫需要大约5倍于干燥10件衬衫的时间。
把所有衣服放在烘箱里:如果我们把所有衣服堆在一起,让周围的温暖做事情,中间的衬衫就需要很长时间才能变干。我不想猜测细节,但我认为这至少是O(N^2)——随着洗涤物负荷的增加,干燥时间增加得更快。
这是否意味着哈希表比数组更快?未必如此。如果您只有很少的条目集合,那么数组可能会更快 - 您可以在检查所有字符串的时间内完成操作,而不仅仅是计算正在查看的字符串的哈希码所需的时间。然而,随着数据集变得越来越大,哈希表最终将击败数组。"
大O描述了一个函数增长行为的上限,例如程序运行时间,当输入变得很大时。
例子:
O(n):如果我将输入大小加倍,则运行时间加倍
O(n2):如果输入大小加倍,则运行时间增加四倍
O(log n):如果输入大小加倍,则运行时间增加一倍
O(2n):如果输入大小增加一,则运行时间加倍
输入大小通常是表示输入所需的位数。
大O符号最常被程序员用来近似衡量计算(算法)需要多长时间才能完成,这个时间将作为输入集大小的函数来表示。
当输入数量增加时,使用大O符号可以比较两种算法的扩展效果。
更精确地说,大O符号 用于表示函数的渐近行为,即函数在趋向无穷大时的行为。
在很多情况下,一个算法的“O”会属于以下几种情况之一:
大O符号忽略那些在输入大小趋近于无穷大时对函数增长曲线没有实质性贡献的因素。这意味着添加到函数中的常数或乘积将被简单地忽略。
Big O是一种“表达”代码“需要多长时间/空间来运行”的通用方法。
你经常会看到O(n),O(n2),O(nlogn)等等,所有这些只是展示算法如何变化的方式。
O(n)表示Big O是n,现在你可能会想,“什么是n!?”那么“n”是元素的数量。比如,如果你想在数组中搜索一个项目,你必须检查每个元素并问“你是正确的元素/项目吗?”在最坏的情况下,该项在最后一个索引处,这意味着它花费了与列表中的项目相同的时间,为了具有普遍性,我们说“嘿,n是一个公平给定的值!”
那么你也许可以理解“n2”的含义,但更加具体地说,想象一下你有一个简单的、最简单的排序算法;冒泡排序。这个算法需要查看整个列表的每个项目。
我的列表
流程如下:
这是O n2,因为你需要查看列表中的所有项目,有“n”个项目。对于每个项目,你需要再次查看所有项目进行比较,这也是“n”,所以对于每个项目,你要查看“n”次,意味着n*n=n2
我希望这就是你想要的简单。
但是请记住,Big O只是一种表达时间和空间的方式。
Big O描述算法的基本缩放特性。
Big O并不能告诉你关于给定算法的所有信息。它仅提供有关算法缩放特性的信息,具体来说,算法的资源使用(例如时间或内存)如何根据“输入大小”缩放。
考虑蒸汽机和火箭之间的区别。它们不仅仅是同一种类型的不同变体(例如Prius发动机与兰博基尼发动机),而是在其核心处具有截然不同的推进系统。蒸汽机可能比玩具火箭快,但是没有蒸汽活塞发动机能够达到轨道运载器的速度。这是因为这些系统具有不同的缩放特性,涉及到达到给定速度(“输入大小”)所需燃料(“资源使用”)的关系。
为什么这么重要?因为软件处理的问题可能相差数十亿倍。想一想这个比例。旅行到月球所需的速度与人类步行速度之间的比率小于10,000:1,与软件可能面临的输入大小范围相比,这绝对微不足道。由于软件可能面临广泛的输入大小范围,因此算法的Big O复杂度,即其基本缩放特性,可能会超越任何实现细节。
考虑典型的排序示例。冒泡排序是O(n 2),而归并排序是O(n log n)。假设您有两个排序应用程序,应用程序A使用冒泡排序,应用程序B使用归并排序,并且对于约30个元素的输入大小,应用程序A在排序方面比应用程序B快1000倍。如果您永远不必排序超过30个元素,那么显然应该选择应用程序A,因为它在这些输入大小上要快得多。但是,如果您发现需要对一千万个项目进行排序,则您预计在这种情况下,由于每种算法的缩放方式不同,应用程序B实际上比应用程序A快数千倍。
这里是我通常使用的英文动物园,用于解释Big-O的常见变种
在所有情况下,都优先选择列表中更高的算法。然而,将成本提高到更昂贵的复杂度类别的成本差异很大。
O(1):
没有增长。无论问题有多大,您都可以在相同的时间内解决它。这与广播有些类似,在广播距离范围内的人数不考虑大小时,需要相同的能量。
O(log n):
这种复杂度与 O(1)相同,只是稍微糟糕一点。在所有实际目的中,您可以将其视为非常大的常数缩放。处理1千个和10亿个项目之间的工作差异仅为六倍。
O(n):
解决问题的成本与问题的规模成比例。如果问题大小加倍,则解决方案的成本也加倍。由于大多数问题都必须以某种方式扫描到计算机中,例如数据输入、磁盘读取或网络流量,因此这通常是一种可行的缩放因子。
O(n log n):
这种复杂度与 O(n)非常相似。在所有实际目的中,这两者是等效的。这个复杂度级别通常仍被认为是可扩展的。通过调整假设,一些 O(n log n)算法可以转化为 O(n)算法。例如,限制键的大小将排序从 O(n log n)减少到 O(n)。
O(n2):
增长是按照正方形的方式进行,其中n是正方形边长。这与"网络效应"的增长率相同,即网络中的每个人可能都认识网络中的其他人。增长是昂贵的。大多数可扩展解决方案无法使用具有这种复杂度水平的算法而不进行重大操作。这通常适用于所有其他的多项式复杂度 - O(nk)。
O(2n):
不具有可扩展性。您没有希望解决任何规模较大的问题。对于专家来说,它有用于了解需要避免的情况,并寻找在O(nk)中的近似算法。
大O表示算法相对于其输入大小使用的时间/空间量度。
如果一个算法是O(n),那么时间/空间的增长速度与其输入是同步的。
如果一个算法是O(n2),那么时间/空间的增长速度是其输入平方倍。
以此类推。
空间复杂度
和时间复杂度
。例如,你可以创建一个需要更多空间但能减少时间复杂度的算法,或者你可以创建一个不需要额外空间的算法,比如原地算法
。在实践中,人们会选择最适合自己需求的算法,无论是为了提高性能还是为了最小化空间占用等。 - James Oravec