大O,您如何计算/近似?


881

大多数拥有CS学位的人肯定会知道Big O代表什么。它可以帮助我们评估算法的可扩展性。

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


4
也许您实际上并不需要提高算法的复杂性,但您至少应该能够对其进行计算来决定……
Xavier Nodet 08/09/23

5
我发现这对Big O,Big Omega和Big Theta的解释非常清楚:xoax.net/comp/sci/algorithms/Lesson6.php
Sam Dutton 2010年

33
-1:叹气,又一次滥用BigOh。BigOh只是一个渐近上限,可以用于任何事物,而不仅与CS有关。将BigOh当作唯一变量来谈论是没有意义的(线性时间算法也是O(n ^ 2),O(n ^ 3)等)。说它可以帮助我们衡量 效率也是一种误导。此外,复杂性类的链接是什么?如果您感兴趣的是计算算法运行时间的技术,那有什么关系呢?

34
Big-O无法衡量效率;它可以衡量算法根据大小进行缩放的程度(它也可以应用于大小以外的其他事物,但这是我们可能感兴趣的)-而且这只是渐近的,因此,如果您运气不好,则算法“较小”-在达到极大数之前,O可能比其他循环慢(如果Big-O适用于循环)。
ILoveFortran 2011年

4
基于Big-O复杂度来选择算法通常是程序设计的重要组成部分。绝对不是 “过早优化”的情况,在任何情况下,这都是一个被滥用的选择性报价。
user207421 '16

Answers:


1480

我会尽力在这里简单地解释它,但要注意,这个主题需要我的学生花几个月的时间才能最终掌握。您可以在《 Java中的数据结构和算法》第二章中找到更多信息。


没有可用于获取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)

该参数Ndata.length值。现在我们需要函数的实际定义f()。这是从源代码完成的,其中每个有趣的行从1到4编号。

有许多方法可以计算BigOh。从这一点出发,我们将假定不依赖于输入数据大小的每个句子都采用恒定C数量的计算步骤。

我们将添加函数的各个步骤,并且局部变量声明和return语句都不依赖于data数组的大小。

这意味着第1行和第4行每个都执行C步,并且函数有点像这样:

f(N) = C + ??? + C

下一部分是定义for语句的值。请记住,我们正在计算计算步骤的数量,这意味着for语句的主体将获得执行N时间。这是一样的添加CN时间:

f(N) = C + (C + C + ... + C) + C = C + N * C + C

没有机械规则来计算for执行主体的次数,您需要通过查看代码的作用来进行计数。为了简化计算,我们忽略了for语句的变量初始化,条件和增量部分。

为了获得实际的BigOh,我们需要对该函数进行渐近分析。大致是这样完成的:

  1. 带走所有常数C
  2. f()获得polynomiumstandard form
  3. 除以多项式的项,然后按增长率对其进行排序。
  4. N靠近的时候保持增长infinity

我们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步骤,我们需要将计数除以二。

f(N) = Summation(i from 1 to 2 * N / 2)( ... ) = 
     = Summation(i from 1 to N)( ... )

句号码2是更加棘手,因为它取决于价值i。看一下:索引i取值:0、2、4、6、8,...,2 * N,并for执行第二个:N倍第一个,N-2第二个,N-4第三个...直到N / 2阶段,第二个for从不执行。

在公式上,这意味着:

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 ) )

(我们假设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执行复杂度恒定。

现在,可以使用一些标识规则来简化求和:

  1. 求和(w从1到N)(C)= N * C
  2. 求和(w从1到N)(A(+/-)B)=求和(w从1到N)(A)(+/-)求和(w从1到N)(B)
  3. 求和(w从1到N)(w * C)= C *求和(w从1到N)(w)(C是一个常数,独立于w
  4. 求和(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²)

6
@arthur那将是O(N ^ 2),因为您将需要一个循环来读取所有列,并需要一个循环来读取特定列的所有行。
Abhishek Dey Das 2014年

@arthur:这取决于。这O(n)其中n是元件或数量O(x*y),其中xy是阵列的尺寸。大哦是“相对于输入”,因此它取决于您的输入。
Mooing Duck 2015年

1
很好的答案,但是我真的很困。求和(i从1到N / 2)(N)怎么变成(N ^ 2/2)?
帕萨,2015年

2
@ParsaAkbari作为一般规则,sum(i从1到a)(b)是* b。这只是b + b + ...(a次)+ b = a * b的另一种说法(根据定义,某些整数乘法的定义)。
马里奥·卡内罗

不太相关,但是只是为了避免混淆,这句话有一个小错误:“索引i取值:0、2、4、6、8,...,2 * N”。索引i实际上上升到2 * N-2,然后循环将停止。
艾伯特

201

大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),因为至多我们将不得不浏览整个列表以找到我们的数字。即使我们可能会第一次尝试找到我们的数字并尝试通过循环一次,Big-O仍为O(n),因为Big-O描述了算法的上限(omega代表下限,theta代表紧密界限) 。

当我们进入嵌套循环时:

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(n ^ 2),是否意味着他将使用嵌套循环?
Navaneeth KN

2
并非如此,导致n平方时间的任何方面都将被视为n ^ 2
asyncwait

@NavaneethKN:您不会总是看到嵌套循环,因为函数调用可以自己O(1)工作。例如,在C标准API中,bsearch本质上是O(log n)strlenO(n)qsortO(n log n)(技术上没有保证,而且quicksort本身的最坏情况复杂度为O(n²),但是假设您的libc作者不是白痴,则其平均情况复杂度为O(n log n),并且使用一种可降低击中O(n²)案件几率的关键选择策略)。两者bsearchqsort能差如果比较功能是病理性的。
ShadowRanger

95

虽然知道如何弄清楚特定问题的解决时间是很有用的,但了解一些一般情况对于帮助您在算法中做出决策可能会大有帮助。

以下是一些最常见的情况,摘自http://en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functions

O(1)-确定数字是偶数还是奇数;使用恒定大小的查找表或哈希表

O(logn)-使用二进制搜索在排序数组中查找项目

O(n)-在未排序的列表中查找项目;加两个n位数字

O(n 2)-用一个简单的算法将两个n位数字相乘;加两个n×n矩阵; 气泡排序或插入排序

O(n 3)-用简单算法将两个n×n矩阵相乘

O(c n)-使用动态规划找到旅行商问题的(精确)解;使用蛮力确定两个逻辑语句是否等效

O(n!)-通过蛮力搜索解决旅行商问题

O(n n)-通常用于代替O(n!)来得出渐近复杂度的更简单公式


为什么不使用x&1==1检查奇数?
Samy Bencherif '17

2
@SamyBencherif:这将是一种典型的检查方式(实际上,只需进行测试x & 1就足够了,不需要进行检查== 1;在C语言中,由于操作符优先级而将其x&1==1求值,因此它实际上与test相同)。我认为您虽然误解了答案;那里是分号,而不是逗号。这并不是说您需要一个用于偶/奇测试的查找表,而是说偶/奇测试检查查找表都是操作。x&(1==1) x&1O(1)
ShadowRanger

我不知道最后一句话中关于用法的主张,但是无论谁这样做,都是用另一个不等效的类替换一个类。类O(n!)包含但严格大于O(n ^ n)。实际的等价关系为O(n!)= O(n ^ ne ^ {-n} sqrt(n))。
conditionalMethod

43

小提醒:该big O符号用于表示渐近复杂度(即,当问题的大小增长到无穷大时),并且它隐藏了一个常数。

这意味着在O(n)中的算法与O(n 2)中的算法之间,最快的方法不一定总是第一个(尽管总存在n值,因此对于大小大于n的问题,第一个算法是最快的)。

注意,隐藏常量很大程度上取决于实现!

同样,在某些情况下,运行时不是输入大小 n的确定性函数。以使用快速排序的排序为例:对n个元素的数组进行排序所需的时间不是常数,而是取决于数组的起始配置。

时间复杂度不同:

  • 最坏的情况(通常最简单,但并不总是很有意义)
  • 一般情况(通常很难找出...)

  • ...

R. Sedgewick和P. Flajolet撰写的《算法分析入门》就是很好的介绍。

如您所说,在优化代码时,应始终使用premature optimisation is the root of all evil和(如果可能)分析。它甚至可以帮助您确定算法的复杂性。


3
在数学中,O(。)表示上限,theta(。)表示上下限。CS中的定义实际上是不同的,还是只是常见的符号滥用?根据数学定义,sqrt(n)既是O(n)又是O(n ^ 2),因此并不总是存在某个n之后的O(n)函数较小的情况。
Douglas Zare 2015年

28

在这里看到答案,我认为我们可以得出结论,我们大多数人确实确实通过查看算法并使用常识而不是使用例如我们在大学时所考虑的主方法进行计算来近似算法的阶数。话虽如此,我必须补充一点,即使是教授也鼓励我们(以后)实际考虑而不是仅仅进行计算。

我也想补充一下递归函数用法

假设我们有一个类似(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)


彼得,回答您提出的问题;我在这里描述的方法实际上可以很好地解决这个问题。但是请记住,这仍然是一个近似值,而不是一个完整的数学正确答案。这里描述的方法也是我们在大学里教过的方法之一,如果我没记错的话,它用于比本示例中使用的阶乘更高级的算法。
当然,这完全取决于您可以对函数主体的运行时间和递归调用的数量进行估算的能力,但是其他方法也是如此。


Sven,我不确定您判断递归函数的复杂性的方法是否适用于更复杂的函数,例如在二叉树中进行自上而下的搜索/求和/操作。当然,您可以考虑一个简单的例子并给出答案。但是我认为您实际上必须为递归数学做一些数学运算?
彼得,

3
递归+1 ...这也很漂亮:“ ...甚至教授也鼓励我们思考...” :)
TT_ 2014年

是的,这太好了。我倾向于这样认为,O(..)内部的术语越高,您正在/机器上进行的工作就越多。在与某事物相关的同时思考它可能是一个近似值,但是这些界限也是如此。他们只是告诉您,当输入数量增加时,要完成的工作又如何增加。
Abhinav Gauniyal 2015年

26

如果您的成本是多项式,则只需保留最高阶项,而无需乘数。例如:

O((N / 2 + 1)*(N / 2))= O(N 2 /4 + N / 2)= O(N 2 /4)= O(N 2

请注意,这对于无限系列不起作用。尽管在某些常见情况下,以下不等式适用于一般情况,但没有单一的配方:

O(log N)<O(N)<O(N log N)<O(N 2)<O(N k)<O(e n)<O(n!)


8
当然O(N)<O(NlogN)
jk。

22

我从信息方面考虑。任何问题都包括学习一定数量的位。

您的基本工具是决策点及其熵的概念。决策点的熵是它将为您提供的平均信息。例如,如果一个程序包含一个具有两个分支的决策点,那么它的熵就是每个分支的概率与该分支的逆概率的对数2之和。这就是您执行该决定所学到的东西。

例如,if具有两个分支的语句(两个可能性均相等)的熵为1/2 * log(2/1)+ 1/2 * log(2/1)= 1/2 * 1 + 1/2 * 1 =1。因此它的熵是1位。

假设您正在搜索N个项目的表,例如N = 1024。那是一个10位的问题,因为log(1024)= 10位。因此,如果您可以使用具有相同可能结果的IF语句进行搜索,则应该做出10个决策。

这就是您通过二进制搜索得到的结果。

假设您正在执行线性搜索。您查看第一个元素,并询问它是否是您想要的元素。概率是1/1024,不是1023/1024。该决策的熵为1/1024 * log(1024/1)+ 1023/1024 * log(1024/1023)= 1/1024 * 10 + 1023/1024 *大约0 =大约0.01位。您学到的很少!第二个决定并不更好。这就是为什么线性搜索如此缓慢的原因。实际上,它是您需要学习的位数的指数。

假设您正在建立索引。假设该表已预先分类为许多bin,并且您使用键中的所有位中的某些位来直接索引该表项。如果有1024个bin,则对于所有1024个可能的结果,熵为1/1024 * log(1024)+ 1/1024 * log(1024)+ ... 这是1/1024 * 10乘以1024个结果,或该索引操作的10位熵。这就是为什么索引搜索速度快的原因。

现在考虑排序。您有N个项目,并且有一个列表。对于每个项目,您必须搜索该项目在列表中的位置,然后将其添加到列表中。因此,排序大约需要基础搜索步骤数的N倍。

因此,基于具有大致相同可能性结果的二元决策的排序全都需要O(N log N)个步骤。如果O(N)排序算法基于索引搜索,则可能是可行的。

我发现几乎所有算法性能问题都可以通过这种方式解决。


哇。您对此有任何有用的参考吗?我觉得这些东西对我设计/重构/调试程序很有帮助。
Jesvin Jose 2011年

3
@aitchnyu:对于它的价值,我写了一本书,涵盖了该主题和其他主题。它已经绝版了很久,但是复制品的价格却合理。我试图让GoogleBooks抢购它,但是目前很难弄清楚谁拥有版权。
Mike Dunlavey

21

让我们从头开始。

首先,接受这样的原理,即可以对数据进行某些简单的操作O(1),即与输入大小无关的时间。这些在C中的原始运算包括

  1. 算术运算(例如+或%)。
  2. 逻辑运算(例如&&)。
  3. 比较操作(例如,<=)。
  4. 结构访问操作(例如,像A [i]这样的数组索引,或者紧跟在->运算符之后的指针)。
  5. 简单分配,例如将值复制到变量中。
  6. 调用库函数(例如scanf,printf)。

要证明此原理的正确性,需要对典型计算机的机器指令(原始步骤)进行详细研究。所描述的每个操作都可以使用少量的机器指令来完成。通常只需要一两个指令。结果,可以及时执行C中的几种语句O(1),即在一定的时间量内独立于输入。这些简单的包括

  1. 在表达式中不包含函数调用的赋值语句。
  2. 阅读声明。
  3. 编写不需要函数调用来评估参数的语句。
  4. 跳转语句中断,继续,转到和返回表达式,其中表达式不包含函数调用。

在C语言中,通过将索引变量初始化为某个值,并在每次循环时将该变量递增1,从而形成许多for循环。当索引达到某个限制时,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循环形式,即最终值和初始值之间差除以index变量递增的量后,将告诉我们循环多少次。除非有某种方法可以通过跳转语句退出循环,否则该计数是准确的。在任何情况下,它都是迭代次数的上限。

例如,for循环进行迭代((n − 1) − 0)/1 = n − 1 times,因为0是i的初始值,n-1是i达到的最高值(即,当i达到n-1时,循环停止,并且在i = n-时不发生迭代1),并在循环的每次迭代中将1加到i。

在最简单的情况下,每次迭代在循环主体中花费的时间是相同的,我们可以将循环主体的big-oh上限乘以循环的次数。严格来说,我们必须添加O(1)时间来初始化循环索引,并添加O(1)时间以将循环索引与limit进行第一次比较,因为我们要比循环遍历多得多的时间。但是,除非可以执行零次循环,否则初始化循环和测试极限一次的时间是可以由求和规则删除的低阶项。


现在考虑以下示例:

(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是否为n,从而得出结论,外循环的每次迭代都需要O(n)时间。

外循环的初始化i = 0和条件i <n的第(n + 1)次测试同样花费O(1)时间,可以忽略。最后,我们观察到我们绕过了外循环n次,每次迭代都花费O(n)时间,从而给出了总的 O(n^2)运行时间。


一个更实际的例子。

在此处输入图片说明


如果goto语句包含一个函数调用怎么办?类似于step3:if(M.step == 3){M = step3(done,M); } step4:if(M.step == 4){M = step4(M); }如果(M.step == 5){M = step5(M); 转到步骤3;}如果(M.step == 6){M = step6(M); 转到步骤4;} return cut_matrix(A,M); 那么如何计算复杂度?是步骤4是n ^ 3,步骤5是n ^ 2,是加法还是乘法?
塔哈·塔里克

14

如果您想凭经验估计代码的顺序而不是通过分析代码,则可以坚持使用一系列递增的n值和时间来增加代码时间。在对数刻度上绘制您的时间。如果代码为O(x ^ n),则值应落在斜率n的直线上。

与仅研究代码相比,这具有多个优点。一方面,您可以查看您是否处于运行时间接近其渐近顺序的范围内。同样,您可能会发现某些您认为是阶O(x)的代码实际上是阶O(x ^ 2),例如,由于在库调用中花费了时间。


只是为了更新此答案:en.wikipedia.org/wiki/Analysis_of_algorithms,此链接包含您需要的公式。如果您有很多算法遵循幂规则,则在机器上有2个时间点和2个运行时,我们可以在对数-对数图上计算斜率。这是a = log(t2 / t1)/ log(n2 / n1),这给了我O(N ^ a)中算法的指数。可以将其与使用代码的手动计算进行比较。
克里斯托弗·约翰

1
嗨,不错的答案。我想知道您是否知道任何库或方法(例如,我使用python / R)来推广这种经验方法,即像将各种复杂性函数拟合为增加大小的数据集一样,并找出哪个是相关的。谢谢
agenis '19

10

基本上,90%的时间都收获的东西只是分析循环。您有单,双,三嵌套循环吗?您有O(n),O(n ^ 2),O(n ^ 3)运行时间。

极少数情况下(除非您正在编写带有扩展基础库的平台(例如.NET BCL或C ++的STL),否则您会遇到比仅查看循环(对于语句,语句,goto,等等...)


1
取决于循环。
kelalaka

8

大O表示法很有用,因为它易于使用并隐藏了不必要的复杂性和细节(对于不必要的某种定义)。树法是解决分而治之算法复杂性的一种不错的方法。假设您有一个带有中值过程的quicksort版本,因此每次都将数组拆分为完全平衡的子数组。

现在,构建一个与您使用的所有阵列相对应的树。在根目录下,您拥有原始数组,在根目录下,有两个子数组,它们是子数组。重复此操作,直到底部有单个元素数组。

由于我们可以找到O(n)时间的中位数,并在O(n)时间中将数组分为两部分,因此每个节点的工作量为O(k),其中k是数组的大小。树的每个级别最多包含整个数组,因此每个级别的工作量为O(n)(子数组的大小总计n,并且由于每个级别有O(k),因此我们可以将其相加) 。自从我们每次将输入减半后,树中只有log(n)级别。

因此,我们可以通过O(n * log(n))来限制工作量。

但是,Big O隐藏了一些有时我们无法忽略的细节。考虑使用以下方法计算斐波那契数列

a=0;
b=1;
for (i = 0; i <n; i++) {
    tmp = b;
    b = a + b;
    a = tmp;
}

并假设a和b是Java中的BigIntegers或可以处理任意大数的东西。大多数人会说这是一种O(n)算法,不会退缩。原因是在for循环中有n次迭代,而O(1)在循环中起作用。

但是斐波那契数很大,第n个斐波那契数在n中是指数的,因此仅存储它就需要n个字节的顺序。用大整数执行加法将需要O(n)的工作量。因此,此过程中完成的总工作量为

1 + 2 + 3 + ... + n = n(n-1)/ 2 = O(n ^ 2)

因此,该算法以四基时间运行!


1
您不必在乎数字的存储方式,算法在O(n)的上限处增长不会改变。
mikek3332002's

8

我认为一般来说不太有用,但是为了完整起见,还有一个Big OmegaΩ(它定义了算法复杂度的下限),还有一个Big ThetaΘ(定义了上限和下限)。


7

将算法分解成您知道大O表示法的部分,然后通过大O运算符进行组合。那是我唯一知道的方法。

有关更多信息,请查看该主题的Wikipedia页面


7

熟悉我使用的算法/数据结构和/或快速浏览迭代嵌套分析。困难在于您可能多次调用库函数-您经常不确定是否有时会不必要地调用该函数或它们正在使用哪种实现。也许库函数应该具有复杂性/效率度量,无论是Big O还是其他度量,可以在文档中甚至在IntelliSense中找到


6

关于“如何计算” Big O,这是计算复杂性理论的一部分。对于某些(许多)特殊情况,您可能可以使用一些简单的启发式方法(例如,将循环计数乘以嵌套循环),尤其是。当您想要的只是任何上限估计,而您也不介意它是否过于悲观时-我想这可能是您的问题所在。

如果您真的想回答任何算法的问题,那么您最好的方法就是运用理论。除了简单的“最坏情况”分析,我发现摊销分析在实践中非常有用。


6

对于第一种情况,内部循环执行了n-i几次,因此执行的总次数是i0到的总和n-1(因为小于或等于)n-i。你终于得到了n*(n + 1) / 2,所以O(n²/2) = O(n²)

对于第二个循环,在外部循环i之间0并在n其中;然后在j严格大于时执行内部循环n,这是不可能的。


5

除了使用master方法(或其专业之一)之外,我还通过实验测试了我的算法。这不能证明可以实现任何特定的复杂性类,但是可以保证数学分析是适当的。为了保证这种安全,我将代码覆盖率工具与实验结合使用,以确保我能正确执行所有案例。

举一个非常简单的例子,您想对.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);

4

不要忘记还允许空间复杂性,如果内存资源有限,空间复杂性也可能引起关注。因此,例如,您可能听到有人想要一个恒定空间算法,这基本上是一种说法,即算法占用的空间量不依赖于代码内的任何因素。

有时,复杂性可能来自于所谓的调用次数,循环执行的频率,内存分配的频率等等,这是回答此问题的另一部分。

最后,大O可以用于最坏情况,最佳情况和摊销情况,其中通常使用最坏情况来描述算法可能有多糟糕。


4

通常被忽略的是算法的预期行为。它不会改变算法的Big-O,但确实与“过早优化.....”语句有关。

您的算法的预期行为-非常笨拙-可以预期您的算法在最有可能看到的数据上运行的速度。

例如,如果您要在列表中搜索值,则为O(n),但是如果您知道大多数列表中都有值,则算法的典型行为会更快。

要真正确定下来,您需要能够描述“输入空间”的概率分布(如果您需要对列表进行排序,那么该列表已经被排序的频率是多少?完全颠倒了多少?多数情况下是排序的吗?)知道这一点并不总是可行的,但有时会做到。


4

好问题!

免责声明:此答案包含错误的陈述,请参阅以下评论。

如果您使用的是Big O,则是在谈论最坏的情况(稍后会详细介绍)。此外,一般情况下有大写字母theta,最佳情况下有大笔欧米茄。

在此站点上可以找到有关Big O的漂亮正式定义:https : //xlinux.nist.gov/dads/HTML/bigOnotation.html

f(n)= O(g(n))表示存在正常数c和k,使得对于所有n≥k,0≤f(n)≤cg(n)。c和k的值必须对于函数f是固定的,并且不得依赖于n。


好的,那么现在我们所说的“最佳情况”和“最坏情况”复杂度是什么意思?

通过示例可以最清楚地说明这一点。例如,如果我们使用线性搜索在排序后的数组中查找数字,那么最坏的情况是我们决定搜索数组的最后一个元素,因为这将花费与数组中所有项一样多的步骤。在最好的情况是,当我们搜索第一要素,因为我们会先检查后进行。

所有这些形容词-情况复杂性的意义在于,我们正在寻找一种方法来根据特定变量的大小来绘制假设程序运行到完成所需的时间。但是,对于许多算法,您可能会争辩说,特定大小的输入不会一次出现。请注意,这与一项功能的基本要求相矛盾,任何输入最多只能有一个输出。因此,我们提出了多个函数来描述算法的复杂性。现在,即使搜索大小为n的数组可能会花费不同的时间,具体取决于您在数组中寻找的内容以及与n成正比的关系,我们仍可以使用最佳情况,平均情况来创建该算法的详尽描述和最坏情况的类。

抱歉,这是如此糟糕,并且缺乏很多技术信息。但是希望它将使时间复杂度类更容易考虑。一旦您对这些感到满意,那么就很容易解析程序,然后寻找诸如for循环之类的事情,这些事情取决于数组的大小,并根据您的数据结构进行推理,什么样的输入会导致琐碎的情况,哪些输入会导致结果。在最坏的情况下。


1
这是不正确的。大O表示“上限”,而不是最坏的情况。
Samy Bencherif '17

1
常见的误解是big-O是指最坏的情况。O和Ω与最坏情况和最佳情况有何关系?
伯恩哈德·巴克

1
这是误导。Big-O表示函数f(n)的上限。Ω表示函数f(n)的下限。它与最佳情况或最坏情况完全无关。
塔斯涅姆·海德

1
您可以将Big-O用作最佳或最坏情况的上限,但除此之外,是没有关系的。
Samy Bencherif

2

我不知道如何以编程方式解决此问题,但人们要做的第一件事是我们对完成的操作数中的某些模式进行算法采样,例如4n ^ 2 + 2n + 1,我们有2条规则:

  1. 如果我们有一个术语的总和,则保留增长率最高的术语,而省略其他术语。
  2. 如果我们有几个因子的乘积,则常量因子将被忽略。

如果简化f(x),其中f(x)是完成的操作数的公式,(4n ^ 2 + 2n + 1上面已解释),我们将在其中获得big-O值[O(n ^ 2)案件]。但这必须考虑程序中的Lagrange插值,这可能很难实现。而且,如果真正的big-O值为O(2 ^ n),又可能有O(x ^ n)之类的东西,那么该算法可能将无法编程。但是如果有人证明我错了,请给我密码。。。。


2

对于代码A,外部循环将执行n+1一段时间,“ 1”时间表示检查我是否仍满足要求的过程。内循环运行的n次数,n-2时间0+2+..+(n-2)+n= (0+n)(n+1)/2= O(n²)

对于代码B,尽管内部循环不会介入并执行foo(),但内部循环将执行n次,具体取决于外部循环的执行时间,即O(n)


1

我想从一些不同的方面来解释Big-O。

Big-O只是用来比较程序的复杂性,这意味着输入增加时它们增长的速度,而不是执行该操作所花费的确切时间。

恕我直言,在big-O公式中,您最好不要使用更复杂的方程式(您可能只是坚持下图中的方程式。)但是,您仍然可以使用其他更精确的公式(例如3 ^ n,n ^ 3,.. 。),但有时可能会误导他人!因此最好使其尽可能简单。

在此处输入图片说明

我想再次强调,这里我们不想为我们的算法获得确切的公式。我们只想显示输入增长时它是如何增长的,并在这种意义上与其他算法进行比较。否则,您最好使用基准测试等其他方法。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.