大多数拥有计算机科学学位的人肯定知道大O代表什么。 它帮助我们衡量一个算法的可扩展性。
但我很好奇,你是如何计算或近似你的算法的复杂性的?
大多数拥有计算机科学学位的人肯定知道大O代表什么。 它帮助我们衡量一个算法的可扩展性。
但我很好奇,你是如何计算或近似你的算法的复杂性的?
当前回答
至于“如何计算”大O,这是计算复杂性理论的一部分。对于一些(许多)特殊的情况,您可能会使用一些简单的启发式方法(例如为嵌套循环乘以循环计数),特别是当您想要的只是任何上限估计时,并且您不介意它是否过于悲观——我猜这可能就是您的问题的内容。
如果你真的想回答任何算法的问题你能做的最好的就是应用这个理论。除了简单的“最坏情况”分析,我发现平摊分析在实践中非常有用。
其他回答
我将尽最大努力在这里简单地解释它,但请注意,这个主题需要我的学生几个月才能最终掌握。你可以在《Java中的数据结构和算法》一书的第2章中找到更多信息。
没有机械程序可以用来获得BigOh。
作为“烹饪书”,要从一段代码中获得BigOh,首先需要意识到您正在创建一个数学公式来计算给定一定大小的输入执行了多少步计算。
目的很简单:从理论的角度比较算法,而不需要执行代码。步数越少,算法越快。
例如,假设你有这样一段代码:
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
}
这个函数返回数组中所有元素的和,我们想创建一个公式来计算该函数的计算复杂度:
Number_Of_Steps = f(N)
我们有f(N),一个计算步数的函数。函数的输入是要处理的结构的大小。这意味着该函数被调用,如:
Number_Of_Steps = f(data.length)
参数N接受数据。长度值。现在我们需要函数f()的实际定义。这是从源代码中完成的,其中每个感兴趣的行编号从1到4。
有很多方法来计算BigOh。从这一点开始,我们将假设每个不依赖于输入数据大小的句子都需要常数C个计算步骤。
我们将添加函数的步数,局部变量声明和return语句都不依赖于数据数组的大小。
这意味着第1行和第4行每一行都要走C步,函数是这样的:
f(N) = C + ??? + C
下一部分是定义for语句的值。请记住,我们正在计算计算步骤的数量,这意味着for语句体被执行N次。这就相当于把C加N次
f(N) = C + (C + C + ... + C) + C = C + N * C + C
没有机械规则来计算for语句体执行了多少次,您需要通过查看代码的操作来计算。为了简化计算,我们忽略了for语句的变量初始化、条件和增量部分。
为了得到实际的BigOh,我们需要函数的渐近分析。大致是这样做的:
去掉所有常数C。 由f()得到多项式的标准形式。 对多项式的项进行除法,并按增长率对它们排序。 保留N趋于无穷时变大的那一个。
f()有两项:
f(N) = 2 * C * N ^ 0 + 1 * C * N ^ 1
去掉所有C常数和冗余部分:
f(N) = 1 + N ^ 1
由于最后一项是当f()接近无穷大时变大的项(考虑极限),这是BigOh参数,sum()函数的BigOh为:
O(N)
有一些技巧可以解决一些棘手的问题:尽可能使用求和。
作为一个例子,这段代码可以很容易地使用求和来求解:
for (i = 0; i < 2*n; i += 2) { // 1
for (j=n; j > i; j--) { // 2
foo(); // 3
}
}
首先需要询问的是foo()的执行顺序。虽然通常是O(1),但你需要问你的教授。O(1)表示(几乎,大部分)常数C,与N大小无关。
第一句中的for语句很复杂。当索引结束于2 * N时,增量为2。这意味着第一个for只执行了N步,我们需要将计数除以2。
f(N) = Summation(i from 1 to 2 * N / 2)( ... ) =
= Summation(i from 1 to N)( ... )
第二句话更棘手,因为它取决于i的值。看一下:索引i取的值:0,2,4,6,8,…, 2 * N,第二个用于执行:N乘以第一个,N - 2是第二个,N - 4是第三个……直到N / 2阶段,在这个阶段,第二个for语句永远不会被执行。
在公式上,这意味着:
f(N) = Summation(i from 1 to N)( Summation(j = ???)( ) )
同样,我们在计算步数。根据定义,每个求和都应该从1开始,以大于等于1的数结束。
f(N) = Summation(i from 1 to N)( Summation(j = 1 to (N - (i - 1) * 2)( C ) )
(我们假设foo()是O(1),并采取C步。)
这里有一个问题:当i取值N / 2 + 1向上时,内部求和以负数结束!这是不可能的,也是错误的。我们需要把和式分成两部分,当i取N / 2 + 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 )
因为关键时刻i > N / 2,内部的for不会被执行,我们假设它的主体上有一个恒定的C执行复杂度。
现在可以使用一些恒等规则来简化求和:
求和(w从1到N)(C) = N * C 求和(w from 1 to N)(A (+/-) B) =求和(w from 1 to N)(A)(+/-)求和(w from 1 to N)(B) 求和(w从1到N)(w * C) = C *求和(w从1到N)(w) (C是一个常数,与w无关) 求和(w从1到N)(w) = (N * (N + 1)) / 2
应用一些代数运算:
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是:
O(N²)
我想从另一个角度来解释Big-O。
Big-O只是用来比较程序的复杂性,也就是当输入增加时它们的增长速度有多快,而不是花在执行操作上的确切时间。
恕我直言,在大o公式中,你最好不要使用更复杂的方程(你可以坚持使用下图中的方程)。然而,你仍然可以使用其他更精确的公式(如3^n, n^3,…),但有时会误导!所以还是尽量简单为好。
我想再次强调,这里我们不想得到一个精确的算法公式。我们只想展示当输入增加时它是如何增长的并在这方面与其他算法进行比较。否则,您最好使用不同的方法,如基准测试。
至于“如何计算”大O,这是计算复杂性理论的一部分。对于一些(许多)特殊的情况,您可能会使用一些简单的启发式方法(例如为嵌套循环乘以循环计数),特别是当您想要的只是任何上限估计时,并且您不介意它是否过于悲观——我猜这可能就是您的问题的内容。
如果你真的想回答任何算法的问题你能做的最好的就是应用这个理论。除了简单的“最坏情况”分析,我发现平摊分析在实践中非常有用。
看到这里的答案,我想我们可以得出这样的结论:我们大多数人确实通过观察它和使用常识来近似算法的顺序,而不是像我们在大学里认为的那样用主方法来计算它。 说了这么多,我必须补充一点,即使教授也鼓励我们(后来)实际思考,而不是仅仅计算。
我还想补充一下如何对递归函数进行处理:
假设我们有这样一个函数(scheme code):
(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)
Peter, to answer your raised issues; the method I describe here actually handles this quite well. But keep in mind that this is still an approximation and not a full mathematically correct answer. The method described here is also one of the methods we were taught at university, and if I remember correctly was used for far more advanced algorithms than the factorial I used in this example. Of course it all depends on how well you can estimate the running time of the body of the function and the number of recursive calls, but that is just as true for the other methods.
好问题!
免责声明:这个答案包含虚假陈述,见下面的评论。
如果您正在使用大O,那么您正在谈论的是最坏的情况(后面将详细介绍它的含义)。此外,在平均情况下有大写的theta,在最佳情况下有大的omega。
你可以在这个网站上找到大O的正式定义:https://xlinux.nist.gov/dads/HTML/bigOnotation.html
f(n) = O(g(n))表示存在正常数c和k,使得当n≥k时0≤f(n)≤cg(n)。对于函数f, c和k的值必须是固定的,且不依赖于n。
好的,那么我们所说的"最佳情况"和"最坏情况"是什么意思呢?
这一点可以通过例子得到最清楚的说明。例如,如果我们使用线性搜索在一个排序数组中查找一个数字,那么最坏的情况是我们决定搜索数组的最后一个元素,因为这将花费与数组中有多少项一样多的步骤。最好的情况是当我们搜索第一个元素时,因为我们将在第一次检查之后完成。
The point of all these adjective-case complexities is that we're looking for a way to graph the amount of time a hypothetical program runs to completion in terms of the size of particular variables. However for many algorithms you can argue that there is not a single time for a particular size of input. Notice that this contradicts with the fundamental requirement of a function, any input should have no more than one output. So we come up with multiple functions to describe an algorithm's complexity. Now, even though searching an array of size n may take varying amounts of time depending on what you're looking for in the array and depending proportionally to n, we can create an informative description of the algorithm using best-case, average-case, and worst-case classes.
抱歉,这是如此糟糕的写作和缺乏太多的技术信息。但希望这能让时间复杂度类更容易理解。一旦你熟悉了这些,你就可以很简单地解析你的程序,寻找像for-loops这样依赖于数组大小的东西,并根据你的数据结构推理什么样的输入会导致简单的情况,什么样的输入会导致最坏的情况。