Don*_*eld 291
一种思考方式是这样的:
O(N ^ 2)意味着对于每个元素,您正在对每个其他元素执行某些操作,例如比较它们.冒泡排序就是一个例子.
O(N log N)意味着对于每个元素,你正在做一些只需要查看元素的log N的东西.这通常是因为您了解了可以让您做出有效选择的元素.最有效的排序就是这样的一个例子,例如合并排序.
O(N!)表示对N个元素的所有可能排列做一些事情.旅行推销员就是一个例子,那里有N!访问节点的方法,暴力解决方案是查看每个可能的排列的总成本,以找到最佳的排列.
bmd*_*cks 264
Big-O表示法对您的代码意味着什么,它是如何在您运行的"事物"数量增加一倍时进行扩展.这是一个具体的例子:
Big-O | computations for 10 things | computations for 100 things ---------------------------------------------------------------------- O(1) | 1 | 1 O(log(n)) | 3 | 7 O(n) | 10 | 100 O(n log(n)) | 30 | 700 O(n^2) | 100 | 10000
所以采取快速排序,即O(n log(n))vs冒泡排序,即O(n ^ 2).排序10件事时,quicksort比冒泡排序快3倍.但是当排序100件事时,它快了14倍!显然,选择最快的算法非常重要.当您到达具有百万行的数据库时,它可能意味着您的查询在0.2秒内执行与使用小时之间的差异.
另一件需要考虑的事情是,糟糕的算法是摩尔定律无法帮助的一件事.例如,如果你有一些科学的计算是O(n ^ 3)并且它每天可以计算100件事,那么加倍处理器的速度只能让你在一天内完成125件事.但是,将计算结果敲到O(n ^ 2),你每天要做1000件事.
澄清: 实际上,Big-O没有说明在相同特定尺寸点的不同算法的比较性能,而是在不同尺寸点的相同算法的比较性能:
computations computations computations Big-O | for 10 things | for 100 things | for 1000 things ---------------------------------------------------------------------- O(1) | 1 | 1 | 1 O(log(n)) | 1 | 3 | 7 O(n) | 1 | 10 | 100 O(n log(n)) | 1 | 33 | 664 O(n^2) | 1 | 100 | 10000
Mat*_*ati 110
您可能会发现将其可视化很有用:

此外,在 LogY/LogX比例上,函数n 1/2,n,n 2 都看起来像直线,而在LogY/X比例2 n,e n,10 n 是直线和n!是线性的(看起来像n log n).
Dom*_*nic 71
这可能太数学了,但这是我的尝试.(我是一名数学家.)
如果某些东西是O(f(n)),那么它在n个元素上的运行时间将等于A f(n)+ B (在时钟周期或CPU操作中测量).理解你还有这些常量A和B是关键,这些常量来自具体的实现.B基本上表示操作的"常量开销",例如,您执行的某些预处理不依赖于集合的大小.A表示实际项目处理算法的速度.
但关键是,你使用大O符号来计算出某些东西的扩展程度.所以这些常数并不重要:如果你想弄清楚如何从10个项目扩展到10000个项目,谁关心不断的开销B?同样,其他问题(见下文)肯定会超过乘法常数A的权重.
所以真正的交易是f(n).如果˚F增长并不与ñ,如˚F(ñ)= 1,那么你会扩展飞驰---你的运行时间将永远只是一个 + 乙.如果f与n线性增长,即f(n)= n,那么你的运行时间将达到最佳值 - 如果你的用户等待10 ns的10个元素,他们将等待10000 ns为10000元素(忽略加性常数).但如果它变得更快,就像n 2那你就麻烦了; 当你收到更大的收藏品时,事情会开始放慢太多.f(n)= n log(n)是一个很好的折衷方案,通常:你的操作不能简单到给出线性缩放,但是你已经设法削减了它,以便它比f更好地扩展(n)= n 2.
实际上,这里有一些很好的例子:
sfi*_*ink 59
don.neufeld的答案非常好,但我可能会分两部分来解释:首先,大多数算法属于O()的粗略层次结构.然后,您可以查看其中的每一个,以得出时间复杂度的典型算法的草图.
出于实际目的,唯一似乎重要的O()是:
就是这样.还有许多其他可能性适合这些(或大于O(2 ^ n)),但它们通常不会在实践中发生,并且它们与其中之一在质量上没有太大差异.立方算法已经有点延伸了; 我只把它们包括在内,因为我经常遇到它们,值得一提(例如矩阵乘法).
这些类算法实际上发生了什么?好吧,我认为你有一个良好的开端,尽管有许多例子不适合这些特征.但对于上述情况,我会说它通常类似于:
这些都不严谨.特别是不是线性时间算法(O(n)):我可以提出一些例子,你必须看看所有的输入,然后是一半的输入,然后是其中的一半,等等.或者反过来说 - - 将输入对折叠在一起,然后递归输出.这些不符合上面的描述,因为你没有看过每个输入一次,但它仍然以线性时间出现.仍有99.2%的时间线性时间意味着每次输入一次.
Joh*_*ner 21
很多这些都很容易用非编程的东西来展示,比如洗牌.
通过穿过整个甲板找到黑桃王牌,然后通过整个甲板找到黑桃2,等等,如果甲板已经向后排序,那么最坏情况n ^ 2将一副纸牌排序.你看了所有52张牌52次.
一般来说,真正糟糕的算法不一定是有意的,它们通常是对其他东西的误用,比如在一些其他方法中调用线性方法,这种方法在线性上重复同一组.
Hen*_*ryR 18
好的 - 这里有一些非常好的答案,但几乎所有这些答案似乎都犯了同样的错误,这是一个普遍存在的问题.
非正式地,我们写f(n)= O(g(n)),如果,直到比例因子,并且对于大于某些n0的所有n,g(n)大于 f(n).也就是说,f(n)不会比g(n)更快地增长,或者从上面增加.这并没有告诉我们f(n)增长有多快,除了保证它不会比g(n)更差.
一个具体的例子:n = O(2 ^ n).我们都知道n的增长速度远远低于2 ^ n,所以我们有权说它由指数函数限制在上面.在n和2 ^ n之间有很多空间,所以它不是一个非常严格的界限,但它仍然是一个合法的界限.
为什么我们(计算机科学家)使用界限而不是精确?因为a)界限通常更容易证明,并且b)它使我们能够简单地表达算法的属性.如果我说我的新算法是O(n.log n),这意味着在最坏的情况下,它的运行时间将被n个输入上的n.log n从上面限制,足够大n(尽管请参阅下面的评论)当我可能不是最坏情况时).
相反,我们想要说函数的增长速度和其他函数一样快,我们使用theta来表示这一点(我会写T(f(n))来表示降价时f(n)的\ Theta) .T(g(n))是用于从上方和下方以g(n)界定的短手,再次,直到缩放因子并且渐近.
即f(n)= T(g(n))<=> f(n)= O(g(n))并且g(n)= O(f(n)).在我们的例子中,我们可以看到n!= T(2 ^ n),因为2 ^ n!= O(n).
为什么要关注这个?因为在你的问题中,你写'有人必须抽烟才能写出一个O(x!)?' 答案是否定的 - 因为基本上你写的所有东西都会被阶乘函数从上面限制.快速排序的运行时间是O(n!) - 它不是一个紧张的界限.
这里还有另一个微妙的维度.通常我们在讨论最坏情况输入时使用O(g(n))表示法,因此我们制作一个复合语句:在最坏的情况下运行时间它不会比采用g(n)的算法更糟糕)步骤,再次模数缩放和足够大的n.但有时我们想谈谈平均甚至最佳案例的运行时间.
像往常一样,Vanilla quicksort就是一个很好的例子.在最坏的情况下它是T(n ^ 2)(它实际上至少需要n ^ 2步,但不会显着更多),但在平均情况下为T(n.log n),也就是说预期的数量为步骤与n.log n成比例.在最好的情况下,它也是T(n.log n) - 但是你可以改进它,例如,检查数组是否已经排序,在这种情况下,最佳情况运行时间将是T(n).
How does this relate to your question about the practical realisations of these bounds? Well, unfortunately, O( ) notation hides constants which real-world implementations have to deal with. So although we can say that, for example, for a T(n^2) operation we have to visit every possible pair of elements, we don't know how many times we have to visit them (except that it's not a function of n). So we could have to visit every pair 10 times, or 10^10 times, and the T(n^2) statement makes no distinction. Lower order functions are also hidden - we could have to visit every pair of elements once, and every individual element 100 times, because n^2 + 100n = T(n^2). The idea behind O( ) notation is that for large enough n, this doesn't matter at all because n^2 gets so much larger than 100n that we don't even notice the impact of 100n on the running time. However, we often deal with 'sufficiently small' n such that constant factors and so on make a real, significant difference.
例如,quicksort(平均成本T(n.log n))和heapsort(平均成本T(n.log n))都是具有相同平均成本的排序算法 - 但快速排序通常比heapsort快得多.这是因为heapsort对每个元素进行了一些比quicksort更多的比较.
这并不是说O()符号是无用的,只是不精确.对于小n来说,这是一个非常直率的工具.
(作为本文的最后一点,请记住O()符号只描述任何函数的增长 - 它不一定是时间,可能是内存,在分布式系统中交换的消息或者需要的CPU数量.并行算法.)
Alb*_*nbo 18
我尝试通过在C#中提供简单的代码示例来解释.
对于 List<int> numbers = new List<int> {1,2,3,4,5,6,7,12,543,7};
O(1)看起来像
return numbers.First();
Run Code Online (Sandbox Code Playgroud)
O(n)看起来像
int result = 0;
foreach (int num in numbers)
{
result += num;
}
return result;
Run Code Online (Sandbox Code Playgroud)
O(n log(n))看起来像
int result = 0;
foreach (int num in numbers)
{
int index = numbers.length - 1;
while (index > 1)
{
// yeah, stupid, but couldn't come up with something more useful :-(
result += numbers[index];
index /= 2;
}
}
return result;
Run Code Online (Sandbox Code Playgroud)
O(n ^ 2)看起来像
int result = 0;
foreach (int outerNum in numbers)
{
foreach (int innerNum in numbers)
{
result += outerNum * innerNum;
}
}
return result;
Run Code Online (Sandbox Code Playgroud)
O(n!)看起来像,呃,厌倦了想出任何简单的东西.
但我希望你得到一般意见?
Ari*_*yck 12
我向非技术朋友描述的方式是这样的:
考虑多位数添加.良好的老式,铅笔和纸张添加.你在7-8岁时学到的那种.给定两个三位或四位数字,您可以相当容易地找出它们相加的内容.
如果我给你两个100位数的数字,然后问你他们加起来了什么,即使你不得不使用铅笔纸,弄清楚它也会非常简单.一个聪明的孩子可以在几分钟内完成这样的补充.这只需要大约100次操作.
现在,考虑多位乘法.你可能在大约8或9岁时就知道了.你(希望)做了很多重复练习来学习它背后的机制.
现在,想象一下,我给了你那两个100位的数字并告诉你将它们相乘.这将是一个很大,很多艰巨的任务,这东西会带你小时做-而且你不可能不出差错的事情.原因是(这个版本的)乘法是O(n ^ 2); 底部数字中的每个数字必须乘以顶部数字中的每个数字,总共大约n ^ 2个操作.在100位数字的情况下,这是10,000次乘法.
不,O(n)算法并不意味着它将对每个元素执行操作.Big-O表示法为您提供了一种方法,可以独立于您的实际机器来讨论算法的"速度".
O(n)表示算法采用的时间随着输入的增加而线性增长.O(n ^ 2)表示算法占用的时间随着输入的平方而增长.等等.
我想到的方式是,你的任务是清理一个挑选N的邪恶恶棍V引起的问题,你必须估计当他增加N时你需要多长时间才能完成你的问题.
O(1) - >增加N确实没有任何差别
O(log(N)) - >每当V加倍N时,你需要花费额外的时间T来完成任务.V再次加倍N,你花费相同的金额.
O(N) - >每当V加倍N时,你花费的时间是原来的两倍.
O(N ^ 2) - >每当V加倍N时,你花费的时间就是4倍.(这不公平!!!)
O(N log(N)) - >每当V加倍N时,你花费两倍的时间加上一点点.
这些是算法的界限; 计算机科学家想要描述它需要多长时间才能获得大的N.(当你考虑加密中使用的数字时,这一点很重要 - 如果计算机加速10倍,那么多少比特就会你必须使用它来确保它仍然需要100年才能打破你的加密而不仅仅是1年?)
如果某些界限对所涉及的人有所影响,则可能会有一些奇怪的表达.对于某些算法,我在Knuth的计算机编程艺术中看到过像O(N log(N)log(log(N))这样的东西.(不记得我的头顶哪一个)
由于某种原因尚未触及的一件事:
当您看到 O(2^n) 或 O(n^3) 或其他令人讨厌的值的算法时,通常意味着您将不得不接受问题的不完美答案才能获得可接受的性能。
在处理优化问题时,像这样爆炸的正确解决方案很常见。在合理的时间内给出的几乎正确的答案比机器腐烂很久之后给出的正确答案要好。
考虑国际象棋:我不知道正确的解决方案是什么,但它可能类似于 O(n^50) 甚至更糟。理论上,任何计算机都不可能真正计算出正确的答案——即使你使用宇宙中的每个粒子作为计算元素,在宇宙生命周期的最短可能时间内执行运算,你仍然剩下很多零。(量子计算机能否解决这个问题是另一回事。)