算法复杂度分析

一 、时间复杂度

           算法复杂度分为时间复杂度空间复杂度。其作用: 时间复杂度是度量算法执行的时间长短;而空间复杂度是度量算法所需存储空间的大小任何算法运行所需要的时间几乎总是取决于他所处理的数据量,在这里我们主要说时间复杂度。对于一个给定计算机的算法程序,我们能画出运行时间的函数图。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)

1. 一般情况下,算法的基本操作重复执行的次数是模块n的某一个函数f(n),因此,算法的时间复杂度记做:T(n)=O(f(n))

  分析:随着模块n的增大,算法执行的时间的增长率和f(n)的增长率成正比,所以f(n)越小,算法的时间复杂度越低,算法的效率越高。
2. 在计算时间复杂度的时候,先找出算法的基本操作,然后根据相应的各语句确定它的执行次数,再找出T(n)的同数量级(它的同数量级有以下:1<Log2n <n <nLog2n <n的平方<n的三次方<2的n次方<n!),找出后,f(n)=该数量级,若T(n)/f(n)求极限可得到一常数c,则时间复杂度T(n)=O(f(n)), 例:
[java]  view plain copy
  1. for(i=1;i<=n;++i)  
  2. {  
  3. for(j=1;j<=n;++j)  
  4. {  
  5. c[ i ][ j ]=0//该步骤属于基本操作 执行次数:n的平方 次  
  6. for(k=1;k<=n;++k)  
  7. c[ i ][ j ]+=a[ i ][ k ]*b[ k ][ j ]; //该步骤属于基本操作 执行次数:n的三次方 次  
  8. }  
  9. }  
  则有 T(n)= n的平方+n的三次方,根据上面括号里的同数量级,我们可以确定 n的三次方 为T(n)的同数量级, 则有f(n)= n的三次方,然后根据T(n)/f(n)求极限可得到常数c。 则该算法的 时间复杂度:T(n)=O(n^3) 注:n^3即是n的3次方。

3.在pascal中比较容易理解,容易计算的方法是:看看有几重for循环,只有一重则时间复杂度为O(n),二重则为O(n^2),依此类推,如果有二分则为O(logn),二分例如快速幂 、二分查找,如果一个for循环套一个二分,那么时间复杂度则为O(nlogn)。
  按数量级递增排列,常见的 时间复杂度 有:
  常数阶O(1),对数阶O(log2n),线性阶O(n),
  线性对数阶O(nlog2n),平方阶O(n^2),立方阶O(n^3),...,
  k次方阶O(n^k), 指数阶O(2^n) 。随着问题规模n的不断增大,上述时间复杂度不断增大, 算法 的执行效率越低。

根据定义,可以归纳出基本的计算步骤 

         1. 计算出基本操作的执行次数T(n) 
            基本操作即算法中的每条语句(以;号作为分割),语句的执行次数也叫做语句的频度。在做算法分析时,一般默认为考虑最坏的情况。

         2. 计算出T(n)的数量级 
            求T(n)的数量级,只要将T(n)进行如下一些操作:
            忽略常量、低次幂和最高次幂的系数,
令f(n)=T(n)的数量级

         3. 用大O来表示时间复杂度 
             当n趋近于无穷大时,如果lim(T(n)/f(n))的值为不等于0的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n))。

   一个示例:

[java]  view plain copy
  1.  int num1, num2;  
  2.  for(int i=0; i<n; i++){   
  3.      num1 += 1;  
  4.     for(int j=1; j<=n; j*=2){   
  5.         num2 += num1;  
  6.     }  
  7. }   
分析:

1.
     语句int num1, num2;的频度为1;
     语句i=0;的频度为1;
     语句i<n; i++; num1+=1; j=1; 的频度为n;
     语句j<=n; j*=2; num2+=num1;的频度为n*log2n;
     T(n) = 2 + 4n + 3n*log2n
2.
      忽略掉T(n)中的常量、低次幂和最高次幂的系数,f(n) = n*log2n
3.
      lim(T(n)/f(n)) = (2+4n+3n*log2n) / (n*log2n)
                     = 2*(1/n)*(1/log2n) + 4*(1/log2n) + 3
当n趋向于无穷大,1/n趋向于0,1/log2n趋向于0
所以极限等于3。
T(n) = O(n*log2n)
简化的计算步骤 
    再来分析一下,可以看出,决定算法复杂度的是执行次数最多的语句,这里是num2 += num1,一般也是最内循环的语句。
并且,通常将求解极限是否为常量也省略掉?
于是,以上步骤可以简化为: 
    1. 找到执行次数最多的语句 
    2. 计算语句执行次数的数量级
    3. 用大O来表示结果
 


    继续以上述算法为例,进行分析:
1.
    执行次数最多的语句为num2 += num1

2.
    T(n) = n*log2n
    f(n) = n*log2n

3.
    // lim(T(n)/f(n)) = 1
    T(n) = O(n*log2n)


  二、插入排序算法的时间复杂度

         现在研究一下插入排序算法的执行时间,按照习惯,输入长度LEN以下用n表示。设循环中各条语句的执行时间分别是c1、c2、c3、c4、c5这样五个常数:

[java]  view plain copy
  1. void insertion_sort(void)           执行时间  
  2. {  
  3.     int i, j, key;  
  4.     for (j = 1; j < LEN; j++) {  
  5.         key = a[j];         c1  
  6.         i = j - 1;          c2  
  7.         while (i >= 0 && a[i] > key) {  
  8.             a[i+1] = a[i];      c3  
  9.             i--;            c4  
  10.         }  
  11.         a[i+1] = key;           c5  
  12.     }  
  13. }  

         显然外层for循环的执行次数是n-1次,假设内层的while循环执行m次,则总的执行时间粗略估计是(n-1)*(c1+c2+c5+m*(c3+c4))。当然,forwhile后面()括号中的赋值和条件判断的执行也需要时间,而我没有设一个常数来表示,这不影响我们的粗略估计。

这里有一个问题,m不是个常数,也不取决于输入长度n,而是取决于具体的输入数据。在最好情况下,数组a的原始数据已经排好序了,while循环一次也不执行,总的执行时间是(c1+c2+c5)*n-(c1+c2+c5),可以表示成an+b的形式,是n的线性函数(Linear Function)。那么在最坏情况(Worst Case)下又如何呢?所谓最坏情况是指数组a的原始数据正好是从大到小排好序的,请读者想一想为什么这是最坏情况,然后把上式中的m替换掉算一下执行时间是多少。

数组a的原始数据属于最好和最坏情况的都比较少见,如果原始数据是随机的,可称为平均情况(Average Case)。如果原始数据是随机的,那么每次循环将已排序的子序列a[1..j-1]与新插入的元素key相比较,子序列中平均都有一半的元素比key大而另一半比key小,请读者把上式中的m替换掉算一下执行时间是多少。最后的结论应该是:在最坏情况和平均情况下,总的执行时间都可以表示成an2+bn+c的形式,是n的二次函数(Quadratic Function)。

在分析算法的时间复杂度时,我们更关心最坏情况而不是最好情况,理由如下:

  1. 最坏情况给出了算法执行时间的上界,我们可以确信,无论给什么输入,算法的执行时间都不会超过这个上界,这样为比较和分析提供了便利。

  2. 对于某些算法,最坏情况是最常发生的情况,例如在数据库中查找某个信息的算法,最坏情况就是数据库中根本不存在该信息,都找遍了也没有,而某些应用场合经常要查找一个信息在数据库中存在不存在。

  3. 虽然最坏情况是一种悲观估计,但是对于很多问题,平均情况和最坏情况的时间复杂度差不多,比如插入排序这个例子,平均情况和最坏情况的时间复杂度都是输入长度n的二次函数。

          比较两个多项式a1n+b1和a2n2+b2n+c2的值(n取正整数)可以得出结论:n的最高次指数是最主要的决定因素,常数项、低次幂项和系数都是次要的。比如100n+1和n2+1,虽然后者的系数小,当n较小时前者的值较大,但是当n>100时,后者的值就远远大于前者了。如果同一个问题可以用两种算法解决,其中一种算法的时间复杂度为线性函数,另一种算法的时间复杂度为二次函数,当问题的输入长度n足够大时,前者明显优于后者。因此我们可以用一种更粗略的方式表示算法的时间复杂度,把系数和低次幂项都省去,线性函数记作Θ(n),二次函数记作Θ(n2)。

Θ(g(n))表示和g(n)同一量级的一类函数,例如所有的二次函数f(n)都和g(n)=n2属于同一量级,都可以用Θ(n2)来表示,甚至有些不是二次函数的也和n2属于同一量级,例如2n2+3lgn。“同一量级”这个概念可以用下图来说明(该图出自[算法导论]):

图 11.2. Θ-notation


         如果可以找到两个正的常数c1和c2,使得n足够大的时候(也就是n≥n0的时候)f(n)总是夹在c1g(n)和c2g(n)之间,就说f(n)和g(n)是同一量级的,f(n)就可以用Θ(g(n))来表示。

以二次函数为例,比如1/2n2-3n,要证明它是属于Θ(n2)这个集合的,我们必须确定c1、c2和n0,这些常数不随n改变,并且当n≥n0以后,c1n2≤1/2n2-3n≤c2n2总是成立的。为此我们从不等式的每一边都除以n2,得到c1≤1/2-3/n≤c2。见下图:

图 11.3. 1/2-3/n


        这样就很容易看出来,无论n取多少,该函数一定小于1/2,因此c2=1/2,当n=6时函数值为0,n>6时该函数都大于0,可以取n0=7,c1=1/14,这样当n≥n0时都有1/2-3/n≥c1。通过这个证明过程可以得出结论,当n足够大时任何an2+bn+c都夹在c1n2和c2n2之间,相对于n2项来说bn+c的影响可以忽略,a可以通过选取合适的c1、c2来补偿。

        几种常见的时间复杂度函数按数量级从小到大的顺序依次是:Θ(lgn),Θ(sqrt(n)),Θ(n),Θ(nlgn),Θ(n2),Θ(n3),Θ(2n),Θ(n!)。其中,lgn通常表示以10为底n的对数,但是对于Θ-notation来说,Θ(lgn)和Θ(log2n)并无区别(想一想这是为什么),在算法分析中lgn通常表示以2为底n的对数。可是什么算法的时间复杂度里会出现lgn呢?回顾插入排序的时间复杂度分析,无非是循环体的执行时间乘以循环次数,只有加和乘运算,怎么会出来lg呢?下一节归并排序的时间复杂度里面就有lg,请读者留心lg运算是从哪出来的。

         除了Θ-notation之外,表示算法的时间复杂度常用的还有一种Big-O notation。我们知道插入排序在最坏情况和平均情况下时间复杂度是Θ(n2),在最好情况下是Θ(n),数量级比Θ(n2)要小,那么总结起来在各种情况下插入排序的时间复杂度是O(n2)。Θ的含义和“等于”类似,而大O的含义和“小于等于”类似。受内存管理机影响,指令的执行时间不一定是常数,但执行时间的上界(Upper Bound)肯定是常数,我们这里假设语句的执行时间是常数只是一个粗略估计。

         

 三、常用的算法的时间复杂度和空间复杂度

排序法

最差时间分析 平均时间复杂度 稳定度 空间复杂度
冒泡排序 O(n2) O(n2) 稳定 O(1)
快速排序 O(n2) O(n*log2n) 不稳定 O(log2n)~O(n)
选择排序 O(n2) O(n2) 稳定 O(1)
二叉树排序 O(n2) O(n*log2n) 不一顶 O(n)

插入排序

O(n2) O(n2) 稳定 O(1)
堆排序 O(n*log2n) O(n*log2n) 不稳定 O(1)
希尔排序 O O 不稳定 O(1)


转自: http://blog.csdn.net/wangjinyu501/article/details/8209492

你可能感兴趣的:(算法,C语言)