时间复杂度

来源:互联网 发布:西翥灵仪 昆明知乎 编辑:程序博客网 时间:2024/05/21 23:34

  • 简述
    • 时间复杂度计算公式
  • 示例
    • 1常数阶
      • code-1 fn O1
      • code-2 fn O1
    • 2线性阶
      • code-3 fn On
      • code-4 fn On
    • 3对数阶
      • code-5 Olog N
      • code-6 ON
    • 4平方阶
      • code-7 ONM
      • code-8 ON2
      • code-9 ON2
  • 小结
    • n问题实例的规模
    • Tn语句执行次数
    • O 表示量级 order
    • T n f n
  • 参考资料


简述

算法复杂度分为时间复杂度和空间复杂度。时间复杂度是指执行算法所需要的计算工作量;而空间复杂度是指执行这个算法所需要的内存空间。
时间复杂度是一个函数,它定性描述了该算法的运行时间。

时间复杂度计算公式:

时间复杂度是总运算次数表达式中受n的变化影响最大的那一项(不含系数),比如:一般总运算次数表达式类似于这样:

a*2^n+b*n^3+c*n^2+d*n*lg(n)+e*n+f

  • a != 0时,时间复杂度就是O(2^n);
  • a=0,b<>0 =>O(n^3);
  • a,b=0,c<>0 =>O(n^2)依此类推

如果你看不懂上面的公式也没关系,看看下面的示例,也许你会明白的更快。


示例

1、常数阶

code-1 f(n) = O(1)

右侧注释中的 num 表示语句执行的次数。

int sum = 0, n = 100;   //运行1次sum = (n+1) * n / 2;    //运行1次printf("%d", sum);      //运行1次

分析:这段代码的运行次数函数是 f(n) = 1 + 1 + 1 ,根据“推导大O阶方法”中的第一条规则,把
1 + 1 + 1 用 1 替换,运行次数函数变成了 f(n) = 1。该函数只有常数项,只需使用规
则1就可以推导出它即这段代码的时间复杂度是 O(1) 。


code-2 f(n) = O(1)

假如 sum = (n+1) * n / 2 执行3次,将上面的代码修改为:

int sum = 0, n = 100;   //运行1次sum = (n+1) * n / 2;    //运行1次sum = (n+1) * n / 2;    //运行1次sum = (n+1) * n / 2;    //运行1次printf("%d", sum);      //运行1次

分析:这段代码的运行次数函数是 f(n) = 1 + 1 + 1 + 1 + 1 。 按照推导大O阶第一条规则,用1取代
所有的加法常数,这段代码的运行次数函数是 f(n) = 1。这段代码的时间复杂度依然是 f(n) = O(1) 。

所有这类代码的时间复杂度都是 O(1)。O(1)叫做常数阶。不存在 O(2) 、 O(9) 这类写法。
线性阶举例运用


2、线性阶

code-3 f(n) = O(n)

int i,sum = 0;for(i = 0; i < n; i++){    // 时间复杂度为O(1)的代码    sum = sum+ i;}

分析:code-3的运行次数函数是 f(n) = n * 1。加法常数为0个,跳过规则一。变量n的最高阶是 n * 1,无其他项,跳过规则二。n * 1中的系数本来就是1,也可以直接跳过规则三,得到code-3的时间复杂度是 f(n) = O(n)。


code-4 f(n) = O(n)

int i;for(i = 0; i < n; i++){    // 时间复杂度为O(1)的代码}int j;for(j = 0; j < m; j++){    // 时间复杂度为O(1)的代码}

分析:code-4的运行次数函数是f(n) = n * 1 + m * 1。
直接跳过规则一。n * 1 + m * 1有两个变量,但次数都是1,任何一项 n * 1 或 m * 1 都可视为最高价;
根据推导规则二“保留最高阶”,得出运行次数函数是f(n) = n * 1 或 f(n) = m * 1;
最后根据规则三,得出code-4的时间复杂度是 f(n) = O(n)。


3、对数阶

code-5 O(log N)

int count = 1;while(count < n){    count = count * 2;    //其他时间复杂度为O(1)的代码 }

code-5似乎不能用前面的推导大O阶方法来分析时间复杂度,我从《数据结构与算法分析》P21找到了分析”运行时间中的对数”的一般法则。这个一般法则是:

如果一个算法用常数时间(O(1)将问题的大小削减为其一部分(通常是1/2),那么该算法就是 O(log N)。
另一方面,如果使用常数时间只是把问题减少一个常数(如将问题减少1),那么这种算法就是 O(N) 。

code-5中,假设 n = 8 ,初始化时,while(count < n) 需要运行8次。经过一次循环后,count变为2,循环需要运行4次,变为原来的一半。根据那条一般法则,判断 code-5 的时间复杂度是 O(log N)。


code-6 O(N)

int count = 1;while(count < n){    count = count + 2;    //其他时间复杂度为O(1)的代码 }

code-6每次执行循环后,会把问题减少2个常数,时间复杂度应为 O(N)。
若将code-6中的count = count + 2改为code = count - 2,时间复杂度仍然是 O(N)。


4、平方阶

code-7 O(N*M)

平方阶举例运用

int i, j;   //运行1次for(i = 0; i < n; i++){     //运行n次    for(j = 0; j < m; j++){   //运行m次        //时间复杂度为O(1)的代码   //运行1次    }    }   

code-7中第二个循环体的时间复杂度是O(N)。第一个循环体将第二个循环体再执行M次,时间复杂度变为O(N*M)。
注意,O(N*M)和O(N2)都叫做平方阶,二者实质相同。

多层循环体的时间复杂度就是每层循环体的运行次数相乘。


code-8 O(N2)

int i, j;for(i = 0; i < n; i++){//运行n次    for(j = i; j < n; j++){//运行 n,n-1,n-2...1 次,总 (n+1)*n/2        //时间复杂度为O(1)的代码    }}

code-8运行次数是(n+1)*n*n/2。
只保留最高阶并且去掉它的系数,时间复杂度是O(N2)。


code-9 O(N2)

void function(int count){    int j;    for(j = count; j < n; j++){        printf("%s", "hello,world");    }}n++;        //运行1次function(n);    //运行n次int i,j;    //运行1次for(i = 0; i < n; i++){  //运行 n*n 次    function(i);}for(i = 0; i < n; i++){  //运行(n+1)*n/2次    for(j = i; j < n; j++){        printf("%s", "hi");    }}

code-9的执行次数(首先忽略掉常数项)是n + n*n + (n+1)*n/2,计算结果为 1.5*n*n + 2*n。只保留最高阶1.5*n*n,最后将系数变为1,执行次数为n*n,时间复杂度为O(N2)。

这种方法有不确定性的因素存在,或者说,在计算执行次数的时候,使用了互相矛盾的方法。


小结

n:问题实例的规模

把复杂性或运行时间表达为n的函数。

T(n):语句执行次数

也称为语句频度时间频度

“O” 表示量级 (order)

“O”表示量级 (order),比如说“二分检索是 O(logn)的”,也就是说它需要“通过logn量级的步骤去检索一个规模为n的数组”。

T (n) = Ο(f (n))

T (n) = Ο(f (n)) 表示存在一个常数C,使得在当n趋于正无穷时总有 T (n) ≤ C * f(n)。
简单来说,当 n增大时,运行时间至多将以正比于 f(n)的速度增长,n趋于正无穷时最大也就跟f(n)差不多大。

常见的算法时间复杂度由小到大依次为:
Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)<…<Ο(2n)<Ο(n!)

注意:这种渐进估计对算法的理论分析和大致比较是非常有价值的,但在实践中细节也可能造成差异。
例如,一个低附加代价的O(n2)算法在n较小的情况下可能比一个高附加代价的 O(nlogn)算法运行得更快。
当然,随着n足够大以后,具有较慢上升函数的算法必然工作得更快。


参考资料

  • 刚刚悟道:算法的时间复杂度推导方法
    http://www.jianshu.com/p/bbb786b19e2e