大多数拥有计算机科学学位的人肯定知道大O代表什么。 它帮助我们衡量一个算法的可扩展性。
但我很好奇,你是如何计算或近似你的算法的复杂性的?
大多数拥有计算机科学学位的人肯定知道大O代表什么。 它帮助我们衡量一个算法的可扩展性。
但我很好奇,你是如何计算或近似你的算法的复杂性的?
当前回答
我不知道如何通过编程来解决这个问题,但人们做的第一件事是我们对算法的特定模式进行抽样,比如4n²+ 2n + 1我们有两个规则:
如果我们有一个项的和,增长率最大的项被保留,其他项被省略。 如果我们有几个因数的乘积,常数因数就省略了。
如果我们化简f(x),其中f(x)是所做操作数量的公式,(上文解释的4n²+ 2n + 1),我们得到大O值[在这种情况下是O(n²)]。但这必须考虑到程序中的拉格朗日插值,这可能很难实现。如果真正的大O值是O(2^n)我们可能有O(x^n)这样的东西,那么这个算法可能是不可编程的。但如果有人证明我错了,给我代码. . . .
其他回答
对于第一种情况,内部循环执行了n-i次,因此执行的总次数是i从0到n-1(因为小于,而不是小于或等于)的和。你得到最后n * (n + 1) / 2,所以O (n²/ 2)= O (n²)。
对于第二个循环,i在0到n之间。然后,当j严格大于n时执行内循环,这是不可能的。
看到这里的答案,我想我们可以得出这样的结论:我们大多数人确实通过观察它和使用常识来近似算法的顺序,而不是像我们在大学里认为的那样用主方法来计算它。 说了这么多,我必须补充一点,即使教授也鼓励我们(后来)实际思考,而不是仅仅计算。
我还想补充一下如何对递归函数进行处理:
假设我们有这样一个函数(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.
除了使用主方法(或其专门化之一)之外,我还通过实验测试了我的算法。这不能证明达到了任何特定的复杂度等级,但它可以保证数学分析是适当的。为了保证这一点,我将代码覆盖工具与我的实验结合起来使用,以确保我使用了所有的案例。
作为一个非常简单的例子,假设你想要对. net框架的列表排序的速度进行完整性检查。你可以像下面这样写,然后在Excel中分析结果,以确保它们不超过n*log(n)曲线。
在这个例子中,我测量了比较的数量,但也要谨慎地检查每个样本量所需的实际时间。然而,您必须更加小心,因为您只是在度量算法,而不包括来自测试基础结构的工件。
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);
如果您希望根据经验而不是通过分析代码来估计代码的顺序,您可以插入一系列不断增加的n值,并为代码计时。在对数刻度上绘制你的时间。如果代码是O(x^n),值应该落在斜率为n的直线上。
这比只研究代码有几个优点。首先,您可以看到您是否在运行时接近其渐近顺序的范围内。此外,您可能会发现一些您认为是O(x)阶的代码实际上是O(x^2)阶的代码,例如,因为花在库调用上的时间。
大O表示算法时间复杂度的上界。它通常与处理数据集(列表)一起使用,但也可以在其他地方使用。
下面是一些在C代码中如何使用它的例子。
假设我们有一个n个元素的数组
int array[n];
如果我们想要访问数组的第一个元素,这将是O(1)因为不管数组有多大,它总是需要相同的常数时间来获得第一项。
x = array[0];
如果我们想在列表中找到一个数字:
for(int i = 0; i < n; i++){
if(array[i] == numToFind){ return i; }
}
这是O(n)因为我们最多要遍历整个列表才能找到我们要的数。大O仍然是O(n),即使我们可能在第一次尝试中找到我们的数字并运行一次循环,因为大O描述了算法的上界(omega是下界,theta是紧界)。
当我们讲到嵌套循环时:
for(int i = 0; i < n; i++){
for(int j = i; j < n; j++){
array[j] += 2;
}
}
这是O(n²)因为对于外层循环的每一次循环(O(n))我们都必须再次遍历整个列表,所以n乘以后只剩下n²。
这仅仅是触及表面,但当你分析更复杂的算法时,涉及证明的复杂数学就会发挥作用。希望这至少能让你熟悉基本知识。