算法时间复杂度计算

Posted 曼陀罗彼岸花

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法时间复杂度计算相关的知识,希望对你有一定的参考价值。

引言

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

排序法

最差时间分析

平均时间复杂度

稳定度

空间复杂度

冒泡排序

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)


一、基础概念

1、时间复杂度

(1)时间频度

一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。

(2)时间复杂度

在刚才提到的时间频度中,n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。但有时我们想知道它变化时呈现什么规律。为此,我们引入时间复杂度概念。一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n))为算法的渐进时间复杂度,简称时间复杂度。

在各种不同算法中,若算法中语句执行次数为一个常数,则时间复杂度为O(1)。另外,在时间频度不相同时,时间复杂度有可能相同,如T(n)=n2+3n+4与T(n)=4n2+2n+1它们的频度不同,但时间复杂度相同,都为O(n2)。按数量级递增排列,常见的时间复杂度有:常数阶O(1),对数阶O(log2n),线性阶O(n),线性对数阶O(nlog2n),平方阶O(n2),立方阶O(n3),..., k次方阶O(nk),指数阶O(2n)。随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。

2、空间复杂度

与时间复杂度类似,空间复杂度是指算法在计算机内执行时所需存储空间的度量。记作: S(n)=O(f(n)) 我们一般所讨论的是除正常占用内存开销外的辅助存储单元规模。讨论方法与时间复杂度类似,不再赘述。

(3)渐进时间复杂度评价算法时间性能

主要用算法时间复杂度的数量级(即算法的渐近时间复杂度)评价一个算法的时间性能。

2、类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)S(n)定义为该算法所耗费的存储空间,它也是问题规模n的函数。渐近空间复杂度也常常简称为空间复杂度。

空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。一个算法在计算机存储器上所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。算法的输入输出数据所占用的存储空间是由要解决的问题决定的,是通过参数表由调用函数传递而来的,它不随本算法的不同而改变。存储算法本身所占用的存储空间与算法书写的长短成正比,要压缩这方面的存储空间,就必须编写出较短的算法。算法在运行过程中临时占用的存储空间随算法的不同而异,有的算法只需要占用少量的临时工作单元,而且不随问题规模的大小而改变,我们称这种算法是“就地\\"进行的,是节省存储的算法,如这一节介绍过的几个算法都是如此;有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如将在第九章介绍的快速排序和归并排序算法就属于这种情况。

如当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1);当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为0(10g2n);当一个算法的空I司复杂度与n成线性比例关系时,可表示为0(n).若形参为数组,则只需要为它分配一个存储由实参传送来的一个地址指针的空间,即一个机器字长空间;若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址,以便由系统自动引用实参变量。


二、求解步骤

⑴找出算法中的基本语句;

算法中执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体。

⑵计算基本语句的执行次数的数量级;

只需计算基本语句执行次数的数量级,这就意味着只要保证基本语句执行次数的函数中的最高次幂正确即可,可以忽略所有低次幂和最高次幂的系数。这样能够简化算法分析,并且使注意力集中在最重要的一点上:增长率。

⑶用大Ο记号表示算法的时间性能。

将基本语句执行次数的数量级放入大Ο记号中。

如果算法中包含嵌套的循环,则基本语句通常是最内层的循环体,如果算法中包含并列的循环,则将并列循环的时间复杂度相加。例如:

for (i=1; i<=n; i++)
      x++;

for (i=1; i<=n; i++)
       for (j=1; j<=n; j++)
               x++;

第一个for循环的时间复杂度为Ο(n),第二个for循环的时间复杂度为Ο(n2),则整个算法的时间复杂度为Ο(n+n2)=Ο(n2)。

常见的算法时间复杂度由小到大依次为:

Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)<…<Ο(2n)<Ο(n!)

Ο(1)表示基本语句的执行次数是一个常数,一般来说,只要算法中不存在循环语句,其时间复杂度就是Ο(1)。Ο(log2n)、Ο(n)、Ο(nlog2n)、Ο(n2)和Ο(n3)称为多项式时间,而Ο(2n)和Ο(n!)称为指数时间。计算机科学家普遍认为前者是有效算法,把这类问题称为P类问题,而把后者称为NP问题。

这只能基本的计算时间复杂度,具体的运行还会与硬件有关。


三、示例

 1)

int num1, num2;
 for(int i=0; i<n; i++){ 
    num1 += 1;
     for(int j=1; j<=n; j*=2){ 
         num2 += num1;
    }
 } 

分析:
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)

 

2)交换i和j的内容
     sum=0;                 (一次)
     for(i=1;i<=n;i++)       (n次 )
        for(j=1;j<=n;j++) (n^2次 )
         sum++;       (n^2次 )
解:T(n)=2n^2+n+1 =O(n^2)

3)  
    for (i=1;i<n;i++)
    {
        y=y+1;         ①   
        for (j=0;j<=(2*n);j++)    
           x++;        ②      
    }         
解: 语句1的频度是n-1
          语句2的频度是(n-1)*(2n+1)=2n^2-n-1
          f(n)=2n^2-n-1+(n-1)=2n^2-2
          该程序的时间复杂度T(n)=O(n^2).         

O(n)      
                                                      
4)
    a=0;
    b=1;                      ①
    for (i=1;i<=n;i++) ②
    {  
       s=a+b;    ③
       b=a;     ④  
       a=s;     ⑤
    }
解:语句1的频度:2,        
           语句2的频度: n,        
          语句3的频度: n-1,        
          语句4的频度:n-1,    
          语句5的频度:n-1,                                  
          T(n)=2+n+3(n-1)=4n-1=O(n).
                                                                                                 
O(log2n )

5)
     i=1;       ①
    while (i<=n)
       i=i*2; ②
解: 语句1的频度是1,  
          设语句2的频度是f(n),   则:2^f(n)<=n;f(n)<=log2n    
          取最大值f(n)= log2n,
          T(n)=O(log2n )

O(n^3)

6)
    for(i=0;i<n;i++)
    {  
       for(j=0;j<i;j++)  
       {
          for(k=0;k<j;k++)
             x=x+2;  
       }
    }
解:当i=m, j=k的时候,内层循环的次数为k当i=m时, j 可以取 0,1,...,m-1 , 所以这里最内循环共进行了0+1+...+m-1=(m-1)m/2次所以,i从0取到n, 则循环共进行了: 0+(1-1)*1/2+...+(n-1)n/2=n(n+1)(n-1)/6所以时间复杂度为O(n^3).
                                  

我们还应该区分算法的最坏情况的行为和期望行为。如快速排序的最 坏情况运行时间是 O(n^2),但期望时间是 O(nlogn)。通过每次都仔细 地选择基准值,我们有可能把平方情况 (即O(n^2)情况)的概率减小到几乎等于 0。在实际中,精心实现的快速排序一般都能以 (O(nlogn)时间运行。
下面是一些常用的记法:


访问数组中的元素是常数时间操作,或说O(1)操作。一个算法如 果能在每个步骤去掉一半数据元素,如二分检索,通常它就取 O(logn)时间。用strcmp比较两个具有n个字符的串需要O(n)时间。常规的矩阵乘算法是O(n^3),因为算出每个元素都需要将n对 元素相乘并加到一起,所有元素的个数是n^2。
指数时间算法通常来源于需要求出所有可能结果。例如,n个元 素的集合共有2n个子集,所以要求出所有子集的算法将是O(2n)的。指数算法一般说来是太复杂了,除非n的值非常小,因为,在 这个问题中增加一个元素就导致运行时间加倍。不幸的是,确实有许多问题 (如著名的“巡回售货员问题” ),到目前为止找到的算法都是指数的。如果我们真的遇到这种情况,通常应该用寻找近似最佳结果的算法替代之。

--------------------------------------------------------------------------------
一些补充说明 
最坏时间复杂度 
    算法的时间复杂度不仅与语句频度有关,还与问题规模及输入实例中各元素的取值有关。一般不特别说明,讨论的时间复杂度均是最坏情况下的时间复杂度。这就保证了算法的运行时间不会比任何更长。

求数量级 
即求对数值(log),默认底数为10,简单来说就是“一个数用标准科学计数法表示后,10的指数”。例如,5000=5x10 3 (log5000=3) ,数量级为3。另外,一个未知数的数量级为其最接近的数量级,即最大可能的数量级。

求极限的技巧 
要利用好1/n。当n趋于无穷大时,1/n趋向于0 

--------------------------------------------------------------------------------
一些规则(引自:时间复杂度计算 ) 
1) 加法规则 
T(n,m) = T1(n) + T2(n) = O (max ( f(n), g(m) )

2) 乘法规则 
T(n,m) = T1(n) * T2(m) = O (f(n) * g(m))

3) 一个特例(问题规模为常量的时间复杂度) 
在大O表示法里面有一个特例,如果T1(n) = O(c), c是一个与n无关的任意常数,T2(n) = O ( f(n) ) 则有
T(n) = T1(n) * T2(n) = O ( c*f(n) ) = O( f(n) )

也就是说,在大O表示法中,任何非0正常数都属于同一数量级,记为O(1)。

4) 一个经验规则 
复杂度与时间效率的关系:
c < log2n < n < n*log2n < n2 < n3 < 2n < 3n < n! (c是一个常量)
|--------------------------|--------------------------|-------------|
          较好                     一般              较差
其中c是一个常量,如果一个算法的复杂度为c 、 log2n 、n 、 n*log2n,那么这个算法时间效率比较高 ,如果是 2n , 3n ,n!,那么稍微大一些的n就会令这个算法不能动了,居于中间的几个则差强人意。


--------------------------------------------------------------------------------------------------
复杂情况的分析 

以上都是对于单个嵌套循环的情况进行分析,但实际上还可能有其他的情况,下面将例举说明。

1.并列循环的复杂度分析 
将各个嵌套循环的时间复杂度相加。

例如:

  for (i=1; i<=n; i++)
      x++;

  for (i=1; i<=n; i++)
      for (j=1; j<=n; j++)
          x++;

解:
第一个for循环
T(n) = n
f(n) = n
时间复杂度为Ο(n)

第二个for循环
T(n) = n2
f(n) = n2
时间复杂度为Ο(n2)

整个算法的时间复杂度为Ο(n+n2) = Ο(n2)。

2.函数调用的复杂度分析 
例如:
public void printsum(int count){
    int sum = 1;
    for(int i= 0; i<n; i++){
       sum += i;
    }   
    System.out.print(sum);
}

分析:
记住,只有可运行的语句才会增加时间复杂度,因此,上面方法里的内容除了循环之外,其余的可运行语句的复杂度都是O(1)。
所以printsum的时间复杂度 = for的O(n)+O(1) = 忽略常量 = O(n)

*这里其实可以运用公式 num = n*(n+1)/2,对算法进行优化,改为:
public void printsum(int count){
    int sum = 1;
    sum = count * (count+1)/2;   
    System.out.print(sum);
}
这样算法的时间复杂度将由原来的O(n)降为O(1),大大地提高了算法的性能。
 

3.混合情况(多个方法调用与循环)的复杂度分析 
例如:
public void suixiangMethod(int n){
    printsum(n);//1.1
    for(int i= 0; i<n; i++){
       printsum(n); //1.2
    }
    for(int i= 0; i<n; i++){
       for(int k=0; k
        System.out.print(i,k); //1.3
      }
  }
suixiangMethod 方法的时间复杂度需要计算方法体的各个成员的复杂度。
也就是1.1+1.2+1.3 = O(1)+O(n)+O(n2) ----> 忽略常数 和 非主要项 == O(n2)

--------------------------------------------------------------------------------------------------
更多的例子 

O(1) 
交换i和j的内容
temp=i;
i=j;
j=temp;                    

以上三条单个语句的频度为1,该程序段的执行时间是一个与问题规模n无关的常数。算法的时间复杂度为常数阶,记作T(n)=O(1)。如果算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)。

O(n2) 
    sum=0;                /* 执行次数1 */
    for(i=1;i<=n;i++)      
       for(j=1;j<=n;j++) 
         sum++;       /* 执行次数n2 */
解:T(n) = 1 + n2 = O(n2)

   for (i=1;i<n;i++)
   { 
       y=y+1;        ①   
       for (j=0;j<=(2*n);j++)    
          x++;        ②      
   }         
解:  语句1的频度是n-1
         语句2的频度是(n-1)*(2n+1) = 2n2-n-1
         T(n) = 2n2-n-1+(n-1) = 2n2-2
         f(n) = n2
         lim(T(n)/f(n)) = 2 + 2*(1/n2) = 2
         T(n) = O(n2).

O(n)                                         
   a=0;
   b=1;                     ①
   for (i=1;i<=n;i++) ②
   {  
      s=a+b;    ③
      b=a;     ④  
      a=s;     ⑤
   }
解:  语句1的频度:2,        
         语句2的频度:n,        
         语句3的频度:n,        
         语句4的频度:n,    
         语句5的频度:n,                                  
         T(n) = 2+4n
         f(n) = n
         lim(T(n)/f(n)) = 2*(1/n) + 4 = 4
         T(n) = O(n).     
                                                                            
O(log2n) 
   i=1;       ①
   while (i<=n)
      i=i*2; ②
解: 语句1的频度是1,  
       设语句2的频度是t,  则:nt<=n;  t<=log2n
       考虑最坏情况,取最大值t=log2n,
        T(n) = 1 + log2n
        f(n) = log2n
        lim(T(n)/f(n)) = 1/log2n + 1 = 1
        T(n) = O(log2n)

 O(n3) 
   for(i=0;i<n;i++)
   {  
      for(j=0;j<i;j++)  
      {
         for(k=0;k<j;k++)
            x=x+2;  
      }
   }
解:当i=m, j=k的时候,内层循环的次数为k当i=m时, j 可以取 0,1,...,m-1 ,  所以这里最内循环共进行了0+1+...+m-1=(m-1)m/2次所以,i从0取到n, 则循环共进行了: 0+(1-1)*1/2+...+(n-1)n/2=n(n+1)(n-1)/2次
T(n) = n(n+1)(n-1)/2 = (n3-n)/2
f(n) = n3
所以时间复杂度为O(n3)。


以上是关于算法时间复杂度计算的主要内容,如果未能解决你的问题,请参考以下文章

多项式计算之秦九韶算法

数据结构与算法-引言

计算时间复杂度例题

计算时间复杂度例题

数据结构与算法基础 第一章 绪论

1 计算方法概论