数据结构与算法(二)-复杂度分析
为什么要进行复杂度分析?
- 和性能测试相比,复杂度分析有不依赖于环境,成本低,效率高,易操作,指导性强的特点.
- 掌握复杂度分析,将能编写出性能更优的代码,有利于降低系统开发和维护成本.
1.时间复杂度
1.1 大O表示法
假如有一段代码:
public void print(int n){
int a = 1; //执行1次
for(int i=0;i<n;++i){//执行n次
System.out.println(a+i);//执行n次
}
}
假设我们执行一行代码平均需要用时是一个常数 t = time,那计算上面代码(算法)的用时,就是执行总行数乘以time。上面的代码总时间就是:
T(n) = (1+2n)*time
其中n是方法参数,代表算法的数据规模。可以看出,执行用时和(1+2n)成正比。我们用大O表示法表示该算法的时间复杂度就是:O(1+2n)
大O表示法会忽略常量、低阶和系数,所以记作:O(n)
大O表示法描述的是随着数据规模n增长时,算法的增长变化趋势。并不代表实际的执行时间。如果算法的执行时间和数据规模n无关,则是常量阶,计作 :O(1)
一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)
1.2 复杂度分析方法
如何计算程序(算法)时间复杂度的问题
1.代码循环次数最多原则
在分析一个算法或者一个代码的时间复杂度时,只需要关注循环次数最多的那一段代码即可.比如O(2*n*n*n + n*n)
的时间复杂为O(n*n*n)
.下面看代码示例:
public void print(int n){
for(int i=0;i<10;++i) System.out.println(i);//循环10次
int i = 1;//一次
while(n>=i) i = i*2;//见下分析
}
2.加法原则
总时间复杂度等于量级最大的那段代码的时间复杂度.比如:
public void print(int n){
//执行100次 属于常量级 忽略
for(int i=0;i<100;++i){
System.out.println(i);
}
//执行n次 时间复杂度O(n)
for(int i=0;i<n;++i){
System.out.println(i);
}
//嵌套循环 执行n*n次 时间复杂度O(n*n)
for(int i=0;i<n;++i){
for(int j=0;j<n;++j){
System.out.println(i+j);
}
}
//嵌套循环 执行n*n+n次 时间复杂度O(n*n)
for(int i=0;i<n;++i){
for(int j=0;j<n;++j){
System.out.println(i+j);
}
System.out.println(i);
}
}
//T(n) = O(n) + O(n*n) = O(n*n+n) = O(n*n)
3.乘法原则
嵌套代码的复杂度等于嵌套内外代码复杂度的乘积,如:
//嵌套循环 执行n*n次 时间复杂度O(n*n)
for(int i=0;i<n;++i){
for(int j=0;j<n;++j){
System.out.println(i+j);
}
}
1.3 常见的时间复杂度
常见的时间复杂度和其数学特性:
大O表示法 | 复杂度量级 |
---|---|
O(1) | 常量阶 |
O(logn) | 对数阶 |
O(n) | 线性阶 |
O(nlogn) | 线性对数阶 |
O(n2)、O(n3)、…O(n^k) | 平方阶、立方阶、…k次方阶 |
O(2^n) | 指数阶 |
O(n!) | 阶乘阶 |
通过曲线的变化,能够直观了解几种时间复杂度的情况。
其中最后两个,指数和阶乘是非多项式量级,其余都是多项式量级。我们把时间复杂度是非多项式量级的算法问题称为NP(Non-Deterministic-Polynomial,非确定多项式)问题。简言之,就是随着数据规模增长,时间复杂度失控,会非常复杂耗时很久的一类问题,有关NP更详细的细节请参考《算法导论》中的章节。有关 “失控”,看下文的拟合曲线直观感受下。
为了更加直观我们取一组样本数据,来拟合一些曲线,更准确直观的感受一下。
首先对比 n的6次方阶底数为2的指数阶 随数据规模n的变化曲线:
在看一卡指数和阶乘对比:
上面我们已经感受了指数量级的恐怖,但是阶乘更加凶残,和阶乘相比,指数让你觉得有一种常量的错觉。具体分析我就不再计算了。
1.4 最好、最坏和平均时间复杂度分析
我利用下面一段代码来进行分析:
//n是数组nums的长度
public int find(int[]nums,int n,int target){
for(int i=0;i<n;++i){
if(nums[i]==target){
return i;
}
}
return -1;
}
-
最好情况。最理想的情况下的复杂度。最理想的情况就是第一个就是要找的,所以最好情况下时间复杂度是O(1)
-
最坏情况。最不理想的情况下的复杂度。就是要找的数在最后一个,所以需要遍历n次,最坏情况下时间复杂度是O(n)
-
平均时间复杂度
分析:为了方便说明,我们假设数组中一定存在要找的数。而要找的数在0 - n-1这些位置出现的概率相同,都是 1/n ,所以考虑每种情况下总共要查找的次数,求出总数,然后再除以 可能的情况数,就是平均要查找的次数,去除常量和系数就是O(n):
-
均摊分析法
平时不太常用,它指的是算法大部分时间是一种复杂度,偶尔会出现复杂度加剧,可以通过将这些偶发情况均摊到出去,计算均摊后的时间复杂度。一般经验告诉我们,均摊后还是一般情况下的复杂度。就好像被“稀释”了。比如栈在动态扩容时,入栈操作时间复杂度由O(1)退化到O(n), 但是扩容是在内存到临界点时才触发一次,所以总的时间复杂度还是O(1).
2.空间复杂度
空间复杂度全程是渐进空间复杂度,表示算法占用的存储空间与数据规模之间的增长关系.下面用一段代码说明一下:
public void print(int n){
int i=0;
int[] a = new int[];
for(int i=0;i<n;i++){
a[i] = i * i;
}
for(int i=0;i<n;i++){
System.out.println(a[i]);
}
}