大多数拥有计算机科学学位的人肯定知道大O代表什么。 它帮助我们衡量一个算法的可扩展性。

但我很好奇,你是如何计算或近似你的算法的复杂性的?


当前回答

我认为,一般来说用处不大,但为了完整起见,还有一个Big Omega Ω,它定义了算法复杂度的下界,还有一个Big Theta Θ,它同时定义了上界和下界。

其他回答

大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²。

这仅仅是触及表面,但当你分析更复杂的算法时,涉及证明的复杂数学就会发挥作用。希望这至少能让你熟悉基本知识。

看到这里的答案,我想我们可以得出这样的结论:我们大多数人确实通过观察它和使用常识来近似算法的顺序,而不是像我们在大学里认为的那样用主方法来计算它。 说了这么多,我必须补充一点,即使教授也鼓励我们(后来)实际思考,而不是仅仅计算。

我还想补充一下如何对递归函数进行处理:

假设我们有这样一个函数(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(1)时间内完成,即在与输入大小无关的时间内完成。C语言中的这些基本操作由

算术运算(例如+或%)。 逻辑操作(如&&)。 比较操作(例如,<=)。 结构访问操作(例如A[i]这样的数组索引,或指针后跟 使用->操作符降低)。 简单的赋值,例如将值复制到变量中。 调用库函数(例如,scanf, printf)。

要证明这一原理,需要对典型计算机的机器指令(基本步骤)进行详细研究。所描述的每一个操作都可以用少量的机器指令来完成;通常只需要一个或两个指令。 因此,C语言中的几种语句可以在O(1)时间内执行,也就是说,在与输入无关的某个常数时间内执行。这些简单的包括

表达式中不涉及函数调用的赋值语句。 读语句。 编写不需要调用函数来计算参数的语句。 跳转语句有break、continue、goto和return表达式 表达式不包含函数调用。

在C语言中,许多for循环是通过将索引变量初始化为某个值和来形成的 在每次循环中对该变量加1。for循环结束于 指数达到某个极限。例如,For循环

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;
}

使用索引变量i。它在循环和迭代中每一次都使i增加1 当I达到n−1时停止。

然而,目前,我们只关注for循环的简单形式,其中最终值和初始值之间的差值除以索引变量的增量,告诉我们循环了多少次。这个计数是准确的,除非有办法通过跳转语句退出循环;在任何情况下,它都是迭代次数的上限。

例如,For循环迭代((n−1)−0)/1 = n−1次, 由于0是i的初始值,n−1是i达到的最大值(即当i 到达n−1时,循环停止,当I = n−1)时不发生迭代,并添加1 在循环的每一次迭代中。

In the simplest case, where the time spent in the loop body is the same for each iteration, we can multiply the big-oh upper bound for the body by the number of times around the loop. Strictly speaking, we must then add O(1) time to initialize the loop index and O(1) time for the first comparison of the loop index with the limit, because we test one more time than we go around the loop. However, unless it is possible to execute the loop zero times, the time to initialize the loop and test the limit once is a low-order term that can be dropped by the summation rule.


现在想想这个例子:

(1) for (j = 0; j < n; j++)
(2)   A[i][j] = 0;

我们知道直线(1)花费O(1)时间。显然,我们循环了n次 我们可以用在线上得到的上限减去下限来确定 (1)再加1。由于主体,行(2),花费O(1)时间,我们可以忽略 增加j的时间和比较j与n的时间,两者都是O(1)。 因此,行(1)和行(2)的运行时间是n和O(1)的乘积,即O(n)。

类似地,我们可以限制由行组成的外部循环的运行时间 (2)到(4),即

(2) for (i = 0; i < n; i++)
(3)     for (j = 0; j < n; j++)
(4)         A[i][j] = 0;

我们已经建立了行(3)和行(4)的循环花费O(n)时间。 因此,我们可以忽略O(1)时间来增加i,并测试i是否< n in 每次迭代,得出每次外循环迭代花费O(n)时间。

外部循环的初始化i = 0和条件的(n + 1)st检验 i < n同样需要O(1)次,可以忽略。最后,我们观察到我们走了 绕外循环n圈,每次迭代花费O(n)时间,得到总数 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)

经常被忽视的是算法的预期行为。它不会改变你的算法的大o,但它确实与“过早优化.. ..”的声明有关

你的算法的预期行为是——非常简单——你期望你的算法在你最有可能看到的数据上工作的速度有多快。

例如,如果你在一个列表中搜索一个值,它是O(n),但如果你知道你看到的大多数列表都有你的值在前面,你的算法的典型行为会更快。

为了真正确定它,你需要能够描述你的“输入空间”的概率分布(如果你需要对一个列表排序,这个列表已经被排序的频率是多少?有多少次是完全相反的?多长时间进行一次排序?)这并不总是可行的,但有时你知道。