大多数拥有计算机科学学位的人肯定知道大O代表什么。 它帮助我们衡量一个算法的可扩展性。
但我很好奇,你是如何计算或近似你的算法的复杂性的?
大多数拥有计算机科学学位的人肯定知道大O代表什么。 它帮助我们衡量一个算法的可扩展性。
但我很好奇,你是如何计算或近似你的算法的复杂性的?
当前回答
对于代码A,外层循环将执行n+1次,“1”时间表示检查i是否仍然满足要求的过程。内循环运行n次,n-2次....因此,0+2+..+(n-2)+n= (0+n)(n+1)/2= O(n²)。
对于代码B,虽然内部循环不会介入并执行foo(),但内部循环将执行n次,这取决于外部循环的执行时间,即O(n)
其他回答
基本上90%的情况下都是分析循环。你有单、双、三重嵌套循环吗?你有O(n) O(n²)O(n³)的运行时间。
很少(除非你正在编写一个具有广泛基库的平台(例如,.NET BCL或c++的STL),你会遇到比查看循环(for语句,while, goto等…)更困难的事情。
除了使用主方法(或其专门化之一)之外,我还通过实验测试了我的算法。这不能证明达到了任何特定的复杂度等级,但它可以保证数学分析是适当的。为了保证这一点,我将代码覆盖工具与我的实验结合起来使用,以确保我使用了所有的案例。
作为一个非常简单的例子,假设你想要对. 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);
大O符号很有用,因为它很容易使用,并且隐藏了不必要的复杂性和细节(对于一些不必要的定义)。求解分治算法复杂性的一种好方法是树法。假设你有一个带有中值过程的快速排序版本,所以你每次都将数组分割成完美平衡的子数组。
现在,构建一个与所使用的所有数组对应的树。根结点有原始数组,根结点有两个子数组。重复此步骤,直到底部有单个元素数组。
由于我们可以在O(n)时间内找到中位数,并在O(n)时间内将数组分成两部分,因此在每个节点上所做的功为O(k),其中k是数组的大小。树的每一层都包含(最多)整个数组,所以每层的功是O(n)(子数组的大小加起来是n,因为每层有O(k),我们可以把它加起来)。树中只有log(n)层,因为每次我们将输入减半。
因此,我们可以将功的上限设为O(n*log(n))。
然而,大O隐藏着一些我们有时不能忽视的细节。考虑计算斐波那契数列
a=0;
b=1;
for (i = 0; i <n; i++) {
tmp = b;
b = a + b;
a = tmp;
}
假设a和b在Java中是biginteger或者其他可以处理任意大数字的东西。大多数人会毫不犹豫地说这是一个O(n)算法。理由是,在for循环中有n次迭代,而O(1)工作在循环的一侧。
但是斐波那契数列很大,第n个斐波那契数列是n的指数级,所以仅仅是存储它就需要n个字节。对大整数执行加法将花费O(n)个工作量。所以在这个过程中所做的总功是
一加二加三……+ n = n(n-1)/2 = O(n)
所以这个算法在二次时间内运行!
对于代码A,外层循环将执行n+1次,“1”时间表示检查i是否仍然满足要求的过程。内循环运行n次,n-2次....因此,0+2+..+(n-2)+n= (0+n)(n+1)/2= O(n²)。
对于代码B,虽然内部循环不会介入并执行foo(),但内部循环将执行n次,这取决于外部循环的执行时间,即O(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.