Big O, how do you calculate/approximate it?
大多数拥有CS学位的人肯定知道大O代表什么。这有助于我们衡量算法的实际效率,如果您知道要解决的问题属于哪一类,那么您就可以确定是否仍有可能挤出那么一点点额外性能。1
但我很好奇,你是如何计算或近似你的算法的复杂性的?
1但是正如他们所说,不要做得太过分,过早的优化是万恶之源,没有正当理由的优化也应该得到这个名字。
我将尽我最大的努力在这里用简单的术语解释它,但请注意,这个主题需要我的学生几个月才能最终掌握。您可以在Java图书的数据结构和算法第2章中找到更多的信息。好的。
没有机械程序可以用来获得Bigoh。好的。
作为一本"食谱",要从一段代码中获得bigoh,首先需要意识到,您正在创建一个数学公式,计算在给定大小的输入下执行的计算步骤的数量。好的。
其目的很简单:从理论上比较算法,而不需要执行代码。步数越小,算法越快。好的。
例如,假设您有这段代码:好的。
1 2 3 4 5 6 7 8 9 | int sum(int* data, int N) { int result = 0; // 1 for (int i = 0; i < N; i++) { // 2 result += data[i]; // 3 } return result; // 4 } |
此函数返回数组中所有元素的总和,我们希望创建一个公式来计算该函数的计算复杂性:好的。
1 | Number_Of_Steps = f(N) |
所以我们有
1 | Number_Of_Steps = f(data.length) |
参数
计算bigoh的方法有很多。从这一点出发,我们将假设每个不依赖于输入数据大小的句子都采用一个恒定的
我们将添加函数的单个步骤数,局部变量声明和返回语句都不取决于
这意味着第1行和第4行中的每一步都要执行C数量的步骤,其功能类似于:好的。
1 | f(N) = C + ??? + C |
下一部分是定义
1 | f(N) = C + (C + C + ... + C) + C = C + N * C + C |
没有机械规则来计算执行
为了得到实际的Bigoh,我们需要对函数进行渐近分析。大致如下:好的。
我们的
1 | f(N) = 2 * C * N ^ 0 + 1 * C * N ^ 1 |
去掉所有的
1 | f(N) = 1 + N ^ 1 |
因为这是一个长期的问题,负荷大时,
1 | O(N) |
有一个解决一些棘手的技巧一:使用summations每当你可以。
作为一个例子,这个代码可以很容易解决:使用summations
1 2 3 4 5 | for (i = 0; i < 2*n; i += 2) { // 1 for (j=n; j > i; j--) { // 2 foo(); // 3 } } |
第一件你需要做的是去执行
在一个单一
1 2 | f(N) = Summation(i from 1 to 2 * N / 2)( ... ) = = Summation(i from 1 to N)( ... ) |
一个两个数,因为它是trickier depends on the
在公式,均值:
1 | f(N) = Summation(i from 1 to N)( Summation(j = ???)( ) ) |
再次,我们计数的数量的步骤。通过定义和求和,你总是在一个开始和结束,在一个比一个更大的或平等的。
1 | f(N) = Summation(i from 1 to N)( Summation(j = 1 to (N - (i - 1) * 2)( C ) ) |
(我们以为这是一
我们在这里有一个问题:当需要的值
1 | f(N) = Summation(i from 1 to N / 2)( Summation(j = 1 to (N - (i - 1) * 2)) * ( C ) ) + Summation(i from 1 to N / 2) * ( C ) |
自
现在可以使用一些summations简体识别规则:
应用一些代数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | f(N) = Summation(i from 1 to N / 2)( (N - (i - 1) * 2) * ( C ) ) + (N / 2)( C ) f(N) = C * Summation(i from 1 to N / 2)( (N - (i - 1) * 2)) + (N / 2)( C ) f(N) = C * (Summation(i from 1 to N / 2)( N ) - Summation(i from 1 to N / 2)( (i - 1) * 2)) + (N / 2)( C ) f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2)( i - 1 )) + (N / 2)( C ) => Summation(i from 1 to N / 2)( i - 1 ) = Summation(i from 1 to N / 2 - 1)( i ) f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2 - 1)( i )) + (N / 2)( C ) f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N / 2 - 1) * (N / 2 - 1 + 1) / 2) ) + (N / 2)( C ) => (N / 2 - 1) * (N / 2 - 1 + 1) / 2 = (N / 2 - 1) * (N / 2) / 2 = ((N ^ 2 / 4) - (N / 2)) / 2 = (N ^ 2 / 8) - (N / 4) f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N ^ 2 / 8) - (N / 4) )) + (N / 2)( C ) f(N) = C * (( N ^ 2 / 2 ) - ( (N ^ 2 / 4) - (N / 2) )) + (N / 2)( C ) f(N) = C * (( N ^ 2 / 2 ) - (N ^ 2 / 4) + (N / 2)) + (N / 2)( C ) f(N) = C * ( N ^ 2 / 4 ) + C * (N / 2) + C * (N / 2) f(N) = C * ( N ^ 2 / 4 ) + 2 * C * (N / 2) f(N) = C * ( N ^ 2 / 4 ) + C * N f(N) = C * 1/4 * N ^ 2 + C * N |
和bigoh是:
1 | O(N2) |
好的。
大O给出了算法时间复杂度的上限。它通常与处理数据集(列表)一起使用,但可以在其他地方使用。
在C代码中使用它的几个例子。
假设我们有n个元素数组
1 | int array[n]; |
如果我们想要访问数组的第一个元素,这将是O(1),因为数组有多大并不重要,所以获取第一个元素总是需要相同的常量时间。
1 | x = array[0]; |
如果我们想在列表中找到一个数字:
1 2 3 | for(int i = 0; i < n; i++){ if(array[i] == numToFind){ return i; } } |
这将是O(N),因为至多我们需要查看整个列表才能找到我们的号码。大O仍然是O(n),尽管我们可能会在第一次尝试时找到我们的数字,并在循环中运行一次,因为大O描述了一个算法的上界(ω表示下界,θ表示紧界)。
当我们进入嵌套循环时:
1 2 3 4 5 | for(int i = 0; i < n; i++){ for(int j = i; j < n; j++){ array[j] += 2; } } |
这是O(n^2),因为对于外部循环(o(n))的每一次,我们都必须再次遍历整个列表,所以n的乘法留给我们n的平方。
这仅仅是表面上的擦伤,但是当你开始分析更复杂的算法时,涉及证明的复杂数学就开始起作用了。希望这至少能让你熟悉基础知识。
虽然知道如何为您的特定问题找出大的O时间是有用的,但是了解一些一般情况对于帮助您在算法中做出决策是很有帮助的。
以下是一些最常见的案例,摘自http://en.wikipedia.org/wiki/big_o_notation orders覕of覕common覕函数:
o(1)-确定数字是偶数还是奇数;使用常量大小的查找表或哈希表
o(logn)-使用二进制搜索在已排序的数组中查找项
o(n)-在未排序的列表中查找项目;添加两个n位数
o(n2)-将两个n位数乘以一个简单算法;添加两个n×n矩阵;冒泡排序或插入排序
o(n3)-用简单算法乘以两个n×n矩阵
o(cn)-使用动态编程找到旅行商问题的(精确)解决方案;使用蛮力确定两个逻辑语句是否等效
O(n)!-通过暴力搜索解决旅行推销员问题
O(n n)-常用于代替O(n!)求渐近复杂度的简单公式
小提示:使用
这意味着在O(n)中的算法和O(n2)中的算法之间,最快的并不总是第一个(尽管总是存在n的值,因此对于大小大于n的问题,第一个算法是最快的)。
注意,隐藏常量很大程度上取决于实现!
此外,在某些情况下,运行时不是输入大小n的确定函数。以使用快速排序为例:对n个元素数组排序所需的时间不是常量,而是取决于数组的初始配置。
有不同的时间复杂性:
- 最坏的情况(通常是最容易理解的,尽管并不总是很有意义的)
一般情况(通常很难弄清楚…)
…
一个很好的介绍是R.Sedgewick和P.Flajolet对算法的分析。
正如您所说,在优化代码时,应该始终使用
看到这里的答案,我想我们可以得出这样的结论:我们中的大多数人确实是通过观察和使用常识来近似算法的顺序,而不是像我们在大学时所想的那样,用主方法来计算。有了这句话,我必须补充一点,即使是教授也鼓励我们(稍后)真正地思考它,而不是仅仅计算它。
另外,我还想添加对递归函数的处理方法:
假设我们有一个类似(方案代码)的函数:
1 2 3 4 | (define (fac n) (if (= n 0) 1 (* n (fac (- n 1))))) |
它递归地计算给定数字的阶乘。
第一步是尝试并确定函数体的性能特征,仅在这种情况下,函数体中不执行任何特殊操作,只执行乘法(或返回值1)。
所以身体的表现是:o(1)(常数)。
接下来,尝试确定递归调用的数量。在这种情况下,我们有n-1递归调用。
所以递归调用的性能是:o(n-1)(顺序是n,因为我们丢弃了不重要的部分)。
然后把这两个放在一起,就可以得到整个递归函数的性能:
1*(n-1)=o(n)
彼得,为了回答你提出的问题,我在这里描述的方法处理得很好。但请记住,这仍然是一个近似值,而不是一个完全正确的数学答案。这里描述的方法也是我们在大学里教的方法之一,如果我记得正确的话,它被用于比我在这个例子中使用的阶乘更高级的算法中。当然,这一切都取决于您如何估计函数体的运行时间和递归调用的数量,但对于其他方法也是如此。
如果您的成本是一个多项式,只需保留最高阶项,而不使用其乘数。例如。:
O((n/2 + 1)*(n/2)) = O(n2/4 + n/2) = O(n2/4) = O(n2)
这不适用于无限系列,请注意。一般情况没有单一的解决方法,但对于某些常见情况,以下不平等适用:
O(log N) < O(N) < O(N log N) < O(N2) < O(Nk) < O(en) < O(n!)
我从信息的角度来考虑。任何问题都包括学习一定数量的位。
您的基本工具是决策点及其熵的概念。决策点的熵是它将给你的平均信息。例如,如果一个程序包含一个具有两个分支的决策点,则其熵是每个分支的概率与该分支的逆概率的log2之和。这就是你通过执行这个决定学到的东西。
例如,一个
假设您正在搜索一个包含n个项目的表,如n=1024。这是一个10位问题,因为日志(1024)=10位。因此,如果你能用同样可能产生结果的if语句来搜索它,它应该会做出10个决定。
这就是二进制搜索的结果。
假设您正在进行线性搜索。你看第一个元素,然后问它是否是你想要的元素。概率是1/1024,而不是1023/1024。该决策的熵是1/1024*对数(1024/1)+1023/1024*对数(1024/1023)=1/1024*10+1023/1024*约0=0.01位。你学得很少!第二个决定并不好。这就是线性搜索如此缓慢的原因。事实上,它是你需要学习的比特数的指数。
假设您正在进行索引。假设该表被预先排序到许多容器中,并且您使用键中的一些所有位直接索引到表条目。如果有1024个容器,则熵为1/1024*对数(1024)+1/1024*对数(1024)+…所有1024个可能的结果。这是1024个结果的1/1024*10倍,或者是一个索引操作的10位熵。这就是索引搜索快速的原因。
现在考虑排序。您有n个项目,并且您有一个列表。对于每个项目,您必须搜索项目在列表中的位置,然后将其添加到列表中。因此,排序大约需要底层搜索步骤数的N倍。
因此,基于具有大致相同可能结果的二元决策进行排序,都需要执行大约O(n logn)个步骤。如果排序算法是基于索引搜索的,那么它是可能的。
我发现几乎所有的算法性能问题都可以用这种方式来研究。
Lets start from the beginning.
首先,接受一些简单的数据操作可以在
这项原则的理由要求对一个典型计算机的机器指令(原始步骤)进行详细研究。所述操作的每一个都可以用一些小数目的机器指令完成;通常只需要一个或两个指令。作为一个结果,在C中的几种声明可以在
In C,many for-loops are formed by initiating an index variable to some value and增量每个环路周围的一个时间变量。回旋结束时索引有一些限制。For instance,the for-loop
1 2 3 4 5 6 7 8 9 10 | for (i = 0; i < n-1; i++) { small = i; for (j = i+1; j < n; j++) if (A[j] < A[small]) small = j; temp = A[small]; A[small] = A[i]; A[i] = temp; } |
Use index variable I.It increments I by 1 time around the loop,and the itements当我到达时停下1
然而,此时此刻,重点放在环路的简单形式上,在环路的最终值和初始值之间的差别,通过增量的可变指数来分解,告诉我们环绕环路的时间是多少。这是正确的,但有一条通过跳跃声明流出环流的途径;这是任何一个案例中的项目数上的一个上限。
For instance,the For-Loop Iterates
在单纯的情况下,在环形身体中的时间是一样的复制这个网站码到您的网站上以设置一个投票箱在您的网站上。周围的时光严格地说,我们必须添加O(1)时间来初始化环形索引与环形索引的第一次比较时间限制,因为我们测试的时间比环绕环路还要长。无尽的可以执行零回路时,启动环路和测试的时间。一次的限制是一个低阶级的条件,可以被summation rule吸收。
Now consider this example:
ZZU1
我们知道线路(1)乘坐
相似的,我们可以绕过由线路组成的外环路的运行时间。(2)through(4),which is
1 2 3 | (2) for (i = 0; i < n; i++) (3) for (j = 0; j < n; j++) (4) A[i][j] = 0; |
我们已经确定,线路的环路(3)和(4)乘以(n)时间。因此,我们可以忽略O(1)时间增量和测试我在哪里每一个迭代,得出结论,外环路的每一个迭代都占用了O(n)时间。
初始化I=0 of the outer loop and the(n+1)st test of the condition我喜欢占用时间,可以忽略。最后,我们注视着我们的离开周围环路一次,用O(N)时间每一次点击,给出一个总数运行时间
A more practical example.
MGX1〔0〕
如果您想根据经验而不是通过分析代码来估计代码的顺序,那么您可以使用一系列增加的n值来计算代码的时间。在对数刻度上绘制计时。如果代码是O(x^n),则值应落在坡度n的直线上。
这比仅仅研究代码有几个优势。首先,您可以看到是否处于运行时间接近其渐近顺序的范围内。此外,您可能会发现一些您认为是O(X)顺序的代码实际上是O(X^2)顺序,例如,因为在库调用中花费了时间。
基本上,90%的时间都是在分析循环。您有单、双、三个嵌套循环吗?你有O(n),O(n^2),O(n^3)运行时间。
非常少(除非你正在编写一个具有广泛基础库的平台(例如,.Net BCL,或者C++的STL),你会遇到比查看你的循环更困难的事情(for语句,而GOTO,等等)。
熟悉我使用的算法/数据结构和/或迭代嵌套的快速浏览分析。困难在于,当您调用一个库函数时,可能会多次调用——您通常不确定是否在某些时候不必要地调用该函数,或者它们使用的是什么实现。也许库函数应该有一个复杂度/效率度量,无论是大O还是其他度量标准,这些度量标准在文档中甚至是IntelliSense中都可用。
一般来说,我认为不太有用,但是为了完整性,还有一个很大的ΩΩ,它定义了算法复杂度的下限,还有一个很大的θΩ,它同时定义了上限和下限。
大O符号是有用的,因为它很容易处理和隐藏不必要的复杂和细节(对于一些不必要的定义)。计算分治算法复杂性的一个好方法是树方法。假设您有一个带有中间过程的Quicksort版本,所以每次都要将数组分割成完全平衡的子数组。
现在,构建一个与您使用的所有数组对应的树。在根目录中,您有原始数组,根目录有两个子目录,它们是子数组。重复此操作,直到底部有单个元素数组。
因为我们可以在O(n)时间中找到中位数,并在O(n)时间中将数组分成两部分,所以在每个节点上完成的工作是O(k),其中k是数组的大小。树的每个级别都包含(最多)整个数组,因此每个级别的工作是O(n)(子数组的大小加起来是N,因为每个级别都有O(k),所以我们可以加起来)。因为每次我们将输入减半,所以树中只有日志(n)级别。
因此,我们可以用o(n*log(n))来限定工作量。
然而,大O隐藏了一些我们有时不能忽视的细节。考虑用
1 2 3 4 5 6 7 | a=0; b=1; for (i = 0; i <n; i++) { tmp = b; b = a + b; a = tmp; } |
让我们假设A和B是Java中的大整数或者可以处理任意大数的事物。大多数人会说这是一个没有退缩的O(N)算法。原因是for循环中有n个迭代,而o(1)在循环的一侧工作。
但是斐波那契数很大,第n个斐波那契数在n中是指数级的,所以只存储它就需要n个字节的顺序。用大整数执行加法需要O(n)个工作量。所以这个过程中完成的总工作量是
1+2+3+…+n=n(n-1)/2=o(n^2)
所以这个算法是以四次方的时间运行的!
把算法分解成你知道的大O符号,并通过大O运算符组合。这是我唯一知道的方法。
有关更多信息,请查看主题的维基百科页面。
至于"如何计算"大O,这是计算复杂性理论的一部分。对于某些(许多)特殊情况,您可以使用一些简单的启发式方法(例如,将循环计数乘以嵌套循环),特别是当您只需要任何上界估计时,您不介意它是否过于悲观-我猜这可能是您的问题所在。
如果你真的想回答任何算法的问题,你能做的最好的就是应用这个理论。除了简单的"最坏情况"分析,我发现摊销分析在实践中非常有用。
对于第一种情况,内环执行
对于第二个循环,
除了使用主方法(或它的一个专门方法),我还通过实验测试了我的算法。这不能证明任何特定的复杂度类是被实现的,但是它可以保证数学分析是适当的。为了帮助消除这种疑虑,我将代码覆盖工具与我的实验结合使用,以确保我正在练习所有的案例。
作为一个非常简单的例子,您希望对.NET框架的列表排序速度进行健全性检查。您可以编写如下内容,然后在Excel中分析结果,以确保它们不超过n*log(n)曲线。
在这个例子中,我度量比较的数量,但也要谨慎地检查每个样本大小所需的实际时间。但是,您必须更加小心,因为您只是在测量算法,而不包括来自您的测试基础结构的工件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | int nCmp = 0; System.Random rnd = new System.Random(); // measure the time required to sort a list of n integers void DoTest(int n) { List<int> lst = new List<int>(n); for( int i=0; i<n; i++ ) lst[i] = rnd.Next(0,1000); // as we sort, keep track of the number of comparisons performed! nCmp = 0; lst.Sort( delegate( int a, int b ) { nCmp++; return (a<b)?-1:((a>b)?1:0)); } System.Console.Writeline("{0},{1}", n, nCmp ); } // Perform measurement for a variety of sample sizes. // It would be prudent to check multiple random samples of each size, but this is OK for a quick sanity check for( int n = 0; n<1000; n++ ) DoTest(n); |
经常被忽视的是算法的预期行为。它不会改变算法的大O值,但它确实与"过早优化"这句话有关。……"
您的算法的预期行为——非常简单——您可以期望您的算法以多快的速度处理您最可能看到的数据。
例如,如果您在列表中搜索一个值,它是O(N),但是如果您知道您看到的大多数列表都有您的值,那么您的算法的典型行为会更快。
要真正确定它,您需要能够描述"输入空间"的概率分布(如果您需要对列表进行排序,那么该列表已经排序的频率是多少?完全颠倒的频率是多少?它通常多久排序一次?)你知道这一点并不总是可行的,但有时你知道。
大问题!
答案:这是disclaimer包含虚假陈述看到下面的评论。
如果你是用大啊,你说的是什么。(有趣的案例-什么是均值以后)。additionally有资本,θ为平均的案例和一个大的欧米茄的最佳案例。
检查出这个网站的一个可爱的大O点的定义:http:/ / / /的HTML xlinux.nist.gov dads / bigonotation.html
f(n) = O(g(n)) means there are positive constants c and k, such that 0 ≤ f(n) ≤ cg(n) for all n ≥ k. The values of c and k must be fixed for the function f and must not depend on n.
好的,所以我们现在怎么均市"最佳案例"和"坏的"complexities吗?
这是我们必须明确说明通算例。例如,如果我们使用的是线性搜索找到一个号码,然后在一个分离的阵列的最坏情况下的冰,当我们决定寻找最后的两个元素的数组,这将把AA AA AA有多步的对象的数组。这将是最坏的,当我们搜索的第一元素,既然我们会做在一张支票。
所有这些个adjective complexities案例是我们的一种方式,寻找两个图的金额一小时hypothetical程序完成在两个术语of the size of独立变量。然而,太多的算法,你可以认为这不是一个单一的有三小时的size of a输入。注意到这contradicts与基本需求的功能,任何输入应该不超过一个输出。所以我们来了两个IP和多功能describe的算法的复杂性。现在,即使搜索的阵列尺寸,可以把时间变化的金额取决于你所要寻找的比例depending阵列和两个N,我们可以创建的信息描述的算法,以最佳的情况下,平均情况和最坏的类。
对不起,这是如此的难写瑞比克·思路特和更多的技术信息。但它会让hopefully复杂性两个小时的课程更容易吗?想。一旦你成为舒服与这些信息成为一个简单的分析物(通过你的程序和寻找的东西-环式开关阵列的大小,depend推理和基于你的数据结构是一种将输入结果的琐碎的用例,输入我的最坏结果的情况。
别忘了也要考虑到空间的复杂性,如果内存资源有限,这也会引起人们的关注。例如,您可能会听到有人想要一个常量空间算法,这基本上是一种表示算法所占用的空间量不依赖于代码中的任何因素的方法。
有时,复杂性可能来自于被调用的次数、执行循环的频率、分配内存的频率,等等是回答这个问题的另一部分。
最后,大O可以用于最坏情况、最佳情况和摊销情况,通常情况下,最坏情况用于描述算法可能有多糟糕。
对于代码A,外循环将执行
对于代码B,虽然内部循环不会介入并执行foo(),但内部循环将执行N次,这取决于外部循环的执行时间,即o(n)
我不知道如何以编程的方式解决这个问题,但人们首先要做的是,我们在完成的操作数量中对特定模式的算法进行采样,例如4n^2+2n+1,我们有两个规则:
如果我们简化f(x),其中f(x)是完成的操作数的公式(上面解释了4n^2+2n+1),我们得到大的o值[o(n^2),在这种情况下]。但这必须考虑到程序中的拉格朗日插值,这可能很难实现。如果真正的大O值是O(2^n),我们可能有类似O(x^n)的值,那么这个算法可能无法编程。但如果有人证明我错了,就给我密码。….
我想解释"大O在点的不同方面。
大O就是比较的复杂性的计划,这是他们如何固定均值生成当输入和更新是不是精确这是花两个小时做的动作。
我的大啊,你最好去槽两个公式的使用更复杂的方程(你可能只是两个棒是在下面的图)。但是你仍然会使用其他更精确的公式(3)^ ^ N,N 3,…),但超过,可以misleading有时!所以,更好的保持它作为简单的作为可能的。
我喜欢emphasize一次,在这里,我们不想让两个精确的公式,为我们的算法。我们只想显示的信息技术的快速发展是当输入和compare与其他算法,在那有道理的。否则你会更好的使用方法不同的台阶状的标记。