数据结构与算法 -- 时间复杂度

来源:互联网 发布:直线制职能制矩阵制 编辑:程序博客网 时间:2024/04/30 22:11

数据结构与算法看完了,回过头来在看时间复杂度,对它们的效率做个比较吧。

一、时间复杂度介绍

1、时间复杂度定义

参看:数据结构-算法-时间复杂度计算

在进行算法分析,语句总得执行次数 T(n) 是关于问题规模 n 的函数,进而分析 T(n) 随 n 的变化情况并确定 T(n) 数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n) = O(f(n)),它表示随问题规模 n 的增大算法执行时间的增长率和 f(n) 的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度,其中 f(n) 是问题规模 n 的某个函数。

2、求解时间复杂度具体步骤

(1)找出算法中的基本语句
算法中执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体。
(2)计算基本语句的执行次数的数量级
只需计算基本语句执行次数的数量级,这就意味着只要保证基本语句执行次数的函数中的最高次幂正确即可。可以忽略所有低次幂和最高次幂的系数。这样能够简化算法分析,并且使注意力集中在最重要的一点上:增长率。
(3)用大 O 记号表示算法的时间性能
将基本语句执行次数的数量级放入大 O 记号中。
大 O 中的 O 的意思就是"order of"(大约是),它是种概念,就比如 大型车、小型车和中型车,忽略具体大小尺寸,来描述汽车。

3、时间复杂度计算方法

(1)用常数 1 取代运行时间中的所有加法常数。
(2)在修改后的运行次数函数中,只保留最高阶项
(3)如果最高阶存在且不是 1,则去除与这个项相乘的常数。最后,得到的最后结果就是时间复杂度。
简单来说,就是保留求出次数的最高次幂,并且把系数去掉,如 T(n) = 2n^2+n+1 = O(n^2)

4、常见的时间复杂度

(1)常数级复杂度:O(1)
(2)对数级复杂度:O(logN)
(3)线性级复杂度:O(N)
(4)线性对数级复杂度:O(NlogN)
(5)平方级复杂度:O(N^2)

复杂度曲线越平越好,越陡越差,常数级复杂度最为理想。
常用的时间复杂度所耗费的时间从小到大依次是:
O(1) < O(logn) < (n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)
举例说明:

执行次数函数

非正式术语

12

O(1)

常数阶

5log2^n + 20

O(logn)

对数阶

2n + 3

O(n)

线性阶

2n + 3nlog2^2 + 19

O(nlogn)

线性对数阶

3n^2 + 2n + 1

O(n^2)    

平方阶

6n^3 + 2n^2 + 3n + 4    

O(n^3)

立方阶

2^n

O(2^n)

指数阶

二、具体说明各种时间复杂度

按照时间复杂度从小到大依次讲解:
讲解之前需要了解一个概念:频度
参看:数据结构频度
在数据结构中,频度是指一个定义变量在它的函数中,并且是它在执行到该段语句为止时,这个定义变量在函数总共执行基本操作的次数。
例如下函数中各行频度n的计算:
for(i=0;i<n;i++) ----------------------------- (1){for(j=0;j<n;j++) ------------------------- (2){c[i][j]=0; ------------------------------ (3)for(k=0;k<n;k++) ------------------- (4){c[i][j]=c[i][j]+a[i][k]*b[k][j]; ------- (5)}}}
(1) for(i=0;i<n;i++)  频度为: n+1
(2) for(j=0;j<n;j++)  频度为:n*(n+1)
(3) c[i][j]=0  频度为:n*n
(4) for(k=0;k<n;k++)  频度为:n*n*(n+1)
(5) c[i][j]=c[i][j]+a[i][k]*b[k][j]  频度为:n*n*n

解释:
(1)i 变量在第一个 for 循环中,从取 i = 0 开始执行,直到i=n-1时为止,至此,i 执行了n次。加上最后i=n跳出循环的判断,故频度共n+1 次;
(2)与(1)不同,当 i 在 0~(n-1) 范围内,内层循环 [即是(2)的for循环] 频度为 n ; 当 i = n 时,内层循环语句没执行。所以相当此时第(1)中 for 循环执行了n次,第二个for 循环执行了n次,加上最后j=n跳出循环的判断,即,频度共 n * (n+1);
(3)此句语句,是要利用(1)、(2)for循环语句的i ,j 对 c[i][j] 进行赋值,此时,i 得到的赋值只有从 0 到 n , j 得到的赋值也是从0到n ,都是 n次,此时(当 i 达到n-1 .\当 j 达到 n-1.)的 i++ \j++都不会执行。 故,频度共 n*n 次;
(4)同上(1),(2)的理由,单独的(4)的for 循环执行了n+1 次,综上,频度为 n*n*(n+1);
(5)同理(3),对于三个for 循环, i 得到的赋值只有从 0 到 n , j 得到的赋值也是从0到n ,k得到的赋值也是从 0 到 n ,即,频度为n*n*n。

1、常数阶(O(1))

#include <stdio.h>int main (void){int n = 10;printf ("n = %d\n", n);printf ("n = %d\n", n);printf ("n = %d\n", n);printf ("n = %d\n", n);printf ("n = %d\n", n);printf ("n = %d\n", n);printf ("n = %d\n", n);printf ("n = %d\n", n);return 0;}
分析:
上述代码中,每条语句的频度均为 1,即 T(n) = O(9),但是一般记作 O(1)。因此它的时间复杂度为 O(1)

(2)对数阶(O(logn))

#include <stdio.h>    int main (void){int i = 1, n = 1000;  //语句1while (i < n)  i = i*2;  //语句2return 0;}
分析:
语句1频度为:1
语句2频度为:f(n),2f(n)<=n;f(n)<=log2n  取最大值 f(n)= log2n
即T(n) = log2n + 1 = O(log2n ),因此它的时间复杂度为 O(logn)

(3)线性阶(O(n))

#include <stdio.h>     int main (void){int i = 0, sum = 0, n = 10;  //语句1for (i = 0; i < n; i++)  //语句2sum += i;  //语句3printf ("sum = %d\n", sum);  //语句4   return 0;}输出结果:sum = 45
分析:
语句1频度为:1
语句2频度为:n+1
语句3频度为:n
语句4频度为:1
即T(n) = 1 + (n + 1) + n + 1 = 2n + 3 = O(n)因此它的时间复杂度为O(n)

(4)线性对数阶(O(nlongn))

#include <stdio.h>  void quick (int arr[], int left, int right)  {         int p = (left + right) / 2;      int pivot = arr[p];      int i = 0, j = 0;      for(i = left, j = right; i < j;)     {                  while (arr[i] < pivot && i < p)              i++;          if (i < p)          {              arr[p] = arr[i];              p = i;        }          while(arr[j] >= pivot && j > p)              j--;          if (j > p)          {              arr[p] = arr[j];              p = j;          }                  arr[p] = pivot;                    if(p - left > 1)              quick (arr, left, p - 1);          if (right - p > 1)              quick (arr, p + 1, right);      }  }  int main()  {      int arr[9] = {20, 8, 25, 3, 15, 9, 30, 5, 22};      quick (arr, 0, 8);      int i = 0;      for(i = 0; i < 9; i++)          printf ("%d ", arr[i]);      printf ("\n");      return 0;  }  输出结果:  3 5 8 9 15 20 22 25 30   
分析:
上述例子为快速排序,如果每次都是均等的划分即T(n)=T(n/2)+T(n/2)+O(n),即每次分成两段,则分的次数为logn,每一次处理需要n次计算,那么时间复杂度就是nlogn
如果每次的划分都是完全不平衡的即T(n)=T(n-1)+O(n),那么快排的时间复杂度是n^2
所以它是不稳定的,因此我们说 快排的平均时间复杂度是nlogn

(5)平方阶(O(n^2))

#include <stdio.h>  int main (void)  {  int i, j, n = 10;  //语句1for (i = 0;i < n; i++)  //语句2{for (j = 0; j < n; j++)  //语句3printf ("*");  printf ("\n");  }    return 0;  }  
分析:
语句1频度为:1
语句2频度为:n + 1
语句3频度为 n * (n + 1)
即T(n) = 1 + (n + 1) + n * (n + 1) = n^2 + 2n + 2 = O(n^2),因此它的时间复杂度为 O(N^2)

(6)立方阶(O(n^3))

#include <stdio.h>  int main (void)  {  int i, j, k, n = 3;  //语句1for (i = 0;i < n; i++)  //语句2{for (j = 0; j < n; j++)  //语句3{for (k = 0; k < n; k++)  //语句4printf ("*");printf ("\n");}}    return 0;  }  
分析:
语句1频度为:1
语句2频度为:n + 1
语句3频度为:n * (n + 1)
语句4频度为:n * n * (n + 1)
即T(n) = 1 + (n + 1) + n * (n + 1) + n * n * (n + 1) = n^3 + 2n^2 + 2n + 2 = O(n^3)
因此它的时间复杂度为 O(n^3)

(7)指数阶(O(2^n))

#include <stdio.h>      int i = 1;    void move (int n, char from, char to) {        printf ("第%d步:将%d号盘子%c---->%c\n", i++, n, from, to);  //语句1      }          void hanoi (int n, char from, char denpend_on, char to)    {          if (n == 1)              move (1, from, to);     else          {              hanoi (n - 1, from, to, denpend_on);         move (n, from, to);             hanoi (n - 1, denpend_on, from, to);        }      }          int main (void)      {          printf ("请输入盘子的个数:\n");          int n;          scanf ("%d", &n);          char x = 'A', y = 'B', z = 'C';          printf ("盘子移动情况如下:\n");          hanoi (n, x, y, z);      } 
分析:
语句1频度为:1 
因此,当 if (n == 1) 时, T(n) = 1 = O(1)
否则,T(n) = 2T(n - 1) + 1 = 2^n + 1 = O(2^n)  因此它的时间复杂度为 O(2^n)
相关递推公式就不写了,写了也看不懂。

三、常用数据结构和算法的时间复杂度

参看:常用数据结构和算法操作效率的对比总结

1、数据结构部分

数据结构中常用的操作的效率表

通用数据结构

查找 

插入 

 删除

遍历 

数组

O(N)

O(N)

O(N)

有序数组

O(logN)

O(N)

O(N)

O(N)

链表

O(N)

O(1)

O(N)

有序链表

O(N)

O(N)

O(N)

O(N)

二叉树

O(logN)

O(logN)

O(logN)

O(N)

二叉树(最坏)

O(N)

O(N)

O(N)

O(N)

红黑树

O(logN)

O(logN)

O(logN)

O(N)

2-3-4树

O(logN)

O(logN)

O(logN)

O(N)

哈希表

O(1)

O(1)

O(1)

专用数据结构

 

 

 

 

O(1)

O(1)

队列

O(1)

O(1)

优先级队列

O(N)

O(1)

优先级队列(堆)

O(logN)

O(logN)

 

2、排序部分

常见的排序算法比较表

排序

平均情况

最好情况

最坏情况

稳定与否

空间复杂度

冒泡排序

O(N2)

O(N)

O(N2)

稳定

1

选择排序

O(N2)

O(N2)

O(N2)

不稳定

1

插入排序

O(N2)

O(N)

O(N2)

稳定

1

希尔排序

O(NlogN)

(依赖于增量序列)

不稳定

1

快速排序

O(NlogN)

O(NlogN)

O(N2)

不稳定

O(logN)

归并排序

O(NlogN)

O(NlogN)

O(NlogN)

稳定

O(N)

二叉树排序

O(NlogN)

O(NlogN)

O(N2)

稳定

O(N)

堆排序

O(NlogN)

O(NlogN)

O(NlogN)

不稳定

1

拓扑排序

O(N+E)

O(N)

3、平均和最坏时间复杂度

(1)最坏时间复杂度
顾名思义,时间复杂度不能再坏了。
以快排为例,在最坏情况下的时间复杂度为T(n) = O(n^2),它表示对于任何输入实例,该算法的运行时间不可能大于0(n^2)
(2)平均时间复杂度
平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下。
还以快排为例,如果每次均等划分,如果每次都是均等的划分即T(n)=T(n/2)+T(n/2)+O(n),即每次分成两段,则分的次数为logn,每一次处理需要n次计算,那么时间复杂度就是nlogn。如果每次的划分都是完全不平衡的即T(n)=T(n-1)+O(n),那么快排的时间复杂度是n^2。但它是不稳定的,无法确保它是否均等划分,因此我们说快排的平均时间复杂度是nlogn。

四、时间复杂度和实际运行时间

时间复杂度和实际运行时间不是一码事,我们在计算时间复杂度的时候,是忽略所有低次幂和最高次幂的系数的。
比如有一个算法,输入n个数据,经过3n^2+log2(n)+n+5次计算得到结果,其时间复杂度为O(n^2)。另外一个算法只需2n^2次计算就能得到结果,其时间复杂度也是O(n^2),但明显比第一个算法要快。
所以说时间复杂度是可以推演计算的,而实际运算时间不可预测。这也是为什么使用时间复杂度而不是使用实际运算时间来判断一个算法的优劣了。
0 0
原创粉丝点击