1.2.2 算法的时间复杂度分析

接下来运用上述推导大O阶的方法进行常见算法的时间复杂度分析。

1.O(1)


void pt(int n)
{ 
     int m=0;              /*执行1次*/   
     m=2*n+n/5+6;            /*执行1次*/   
     printf("%d",m);         /*执行1次*/ 
 } 

这个算法是由使用顺序结构的语句构成的,算法的运行次数函数T(n)=3。根据大O阶的推导步骤,把常数项3改为1,因此求出该算法的时间复杂度为O(1)。

假设现在修改该算法,执行语句m=2*n+n/5+6五遍,算法如下:


void pt(int n)
 { 
     int m=0;              /*执行1次*/   
     m=2*n+n/5+6;            /*执行1次*/  
     m=2*n+n/5+6;            /*执行1次*/   
     m=2*n+n/5+6;            /*执行1次*/   
     m=2*n+n/5+6;            /*执行1次*/   
     m=2*n+n/5+6;            /*执行1次*/   
     printf("%d",m);         /*执行1次*/ 
  } 

这个算法的运行次数函数T(n)=7。根据大O阶的推导步骤,把常数项7改为1,因此求出该算法的时间复杂度也是O(1)。

由上面两个例子可以看出,不管n的值是多少,第一个算法和第二个算法的执行次数始终都是3次和7次,这类算法的执行次数与问题规模n没有关系,算法的时间复杂度始终为O(1)。

2.O(n)

在算法的基本结构中,主要有三种:顺序结构、选择结构和循环结构。顺序结构的语句一般都是O(1)(循环体内的除外)。选择结构中,无论选择哪个分支运行,执行次数都是不变的,不会随着n的变大而增加,因此选择结构一般也是O(1)(循环体内的除外)。循环结构相对于前两者来说就会复杂一些,我们需要计算出基本语句的运行次数,以此确定算法的阶次,此时需要分析循环结构内基本语句的执行情况。


void sum(int n)
 { 
     int i;                    /*执行1次*/ 
     int sum=0;              /*执行1次*/ 
     for(i=0; i<n; i++) 
     {                 
          sum=sum+i;         /*执行n次*/   
     }                   
     printf("%d", sum);        /*执行1次*/ 
  } 

这个算法的运行次数函数T(n)=n+3。根据大O阶的推导步骤,求出该算法的时间复杂度是O(n)。此时算法的执行次数随问题规模n的增加而增加,两者是线性关系,因此称这类算法的时间复杂度为线性阶。

3.O(log2n)


void power sum(int n)
 { 
     int i=0;                  /*执行1次*/ 
     int sum=0;              /*执行1次*/ 
     while(i<n) 
     {                 
         sum=sum+i;          /*执行log2n次*/ 
         i=i*2;                /*执行log2n次*/ 
     }                   
     printf("%d", sum);        /*执行1次*/ 
  } 

这个算法的关键在于分析循环体内基本语句的执行次数,即计算循环的执行次数。循环变量i的初始值为1,每次循环让i=i×2,循环条件也就转换为判断多少个2相乘后大于n,如果满足条件,就结束循环。假设循环次数为c,由于2×c=n,算出c=log2n。由此算出该算法的运行次数函数T(n)=2×log2n+3。根据大O阶的推导步骤,求出该算法的时间复杂度是O(log2n)。

4.O(n2)


void sum1(int n)
 {   
     int i,j;                         /*执行1次*/ 
     int sum=0;                     /*执行1次*/ 
     for(i=0; i<n; i++) 
        for(j=0; i<n; j++)
           {                 
               sum=sum+i*j;         /*执行n2次*/   
           }                   
     printf("%d", sum);               /*执行1次*/ 
  } 

这个算法使用了双重循环,内层循环在前面已经分析过,时间复杂度是O(n)。在内层循环的外面再加上一层循环,其实也就是把循环体内时间复杂度为O(n)的语句,再执行n次,因此得出该算法的时间复杂度为O(n2)。

下面对这个算法进行修改:


void sum2(int m, int n)
{ 
     int i,j;                         /*执行1次*/ 
     int sum=0;                     /*执行1次*/ 
     for(i=0; i<m; i++) 
        for(j=0; j<n; j++)
           {                 
                sum=sum+i*j;        /*执行m*n次*/   
           }                   
     printf("%d", sum);               /*执行1次*/ 
 } 

此时这个算法仍采用双重循环,内层循环的时间复杂度还是O(n),只不过外层循环的执行次数变成了m次,也就是把循环体内时间复杂度为O(n)的语句再执行m次,因此得出该算法的时间复杂度为O(m×n)。

再来看看下面这个算法,它的时间复杂度是多少呢?


void sum3(int n)
{ 
     int i,j;                         /*执行1次*/ 
     int sum=0;                     /*执行1次*/ 
     for(i=0; i<n; i++) 
        for(j=i; j<n; j++)
           {                 
                sum=sum+i*j;        /*执行(n2+n)/2次*/   
           }                   
     printf("%d", sum);               /*执行1次*/ 
 } 

这个算法同样采用了双重循环,但是内层循环的执行次数不再是n了,需要重新计算,此时内层循环的执行次数与i的值有关。当i=0时,内层循环的执行次数是n;当i=1时,内循环的执行次数是n-1;当i=2时,内循环的执行次数是n-2;以此类推,当i=n-1时,内循环的执行次数是1次,总的执行次数为:

T(n)=n+(n-1)+(n-2)+…+1+3=(n2+n)/2+3

根据大O阶的推导步骤,得出该算法的时间复杂度是O(n2)。

刚才介绍了O(1)、O(n)、O(log2n)和O(n2),除了上述四种以外,还有O(n log2n)、O(n3)等时间复杂度。下面按由快到慢的顺序列出当n足够大时,常见的几种大O阶运行时间并进行对比,详见图1.5和表1.7:

O(log2n)<O(n)<O(n log2n)<O(n2)<…<O(2n)<O(n!)

图1.5 常见的大O函数图

表1.7 常见的大O运行时间

一个问题的解可能有多个,这时候在选择和设计算法的时候就要尽量选择时间复杂度低的算法。上述算法中,O(2n)和O(n !)是不切实际的时间复杂度,理论上可行,但实际上不可行,在解决问题时应当尽量避免使用此类算法。