1.为何需要复杂度分析
我们大可以将代码跑一便,通过统计、监控等手段可以得到算法执行时间和占用内存情况,为什么还要做时间空间复杂度分析呢?这种方法其实也是正确的,叫做事后统计法
,但是这种方法具有非常大的局限性
,其原因如下:
- 测试结果非常依赖
测试环境
- 测试结果受
数据规模
影响较大
所以,我们通常使用时间复杂度
和空间复杂度
两种方式表示算法的复杂度
2.大O时间复杂度表示
int cal(int n){
int sum = 0;
int i = 1;
for(; i <= n; ++ i){
sum = sum + i;
}
return sum;
}
把CPU执行每行代码的时间看做相同,称为unit_time,如上代码时间复杂度:T(n) = 2n + 4,用大O时间复杂度表示:T(n) = O(n)
一段代码执行时间与行数有关,所以抽象出一个公式:
T(n) = O(f(n))
其中O表示代码的执行时间T(n)与f(n)表达式成正比
- 也就是
所有代码的执行时间T(n)与每行代码执行次数n成正比
- 大O时间复杂度
代表的不是代码真正的执行时间,而是代码执行时间随数据规模增长的一种渐进趋势
,所以也叫做渐进时间复杂度
,简称时间复杂度 大O时间复杂度只需要记录最大量级
int cal(int n){
int sum = 0;
int i = 1;
int j = 1;
for(i = 1; i < n; ++ i){
j = 1;
for(j = 1; j <= n; ++ j){
sum = sum + i;
}
}
return sum;
}
上述代码时间复杂度:T(n) = O(2n^2 + 2n + 3),用大O时间复杂度表示:T(n) = O(n ^ 2)
3.常用的分析时间复杂度的方法
3.1.只关心循环次数最多的代码
适用于代码中只要一个循环体
int cal(int n){
int sum = 0;
int i = 1;
for(; i <= n; ++ i){
sum = sum + i;
}
return sum;
}
T(n) = O(n)
3.2.加法法则,总复杂度等于量级最大的代码
适用于代码中有多个循环体
int cal(int n){
int sum_1 = 0;
int p = 1;
for(; p < 100; ++ p){
sum_1 = sum_1 + p;
}
int sum_2 = 0;
int q = 1;
for(; q < 100; ++ q){
sum_2 = sum_2 + q;
}
int sum_3 = 0;
int i = 1;
int j = 1;
for(; i <= n; ++ i){
j = 1;
for(; j <= n; ++ j){
sum_3 = sum_3 + i * j;
}
}
return sum_1 + sum_2 + sum_3;
}
T(n) = O(n ^ 2)
3.3.乘法法则,嵌套代码的复杂度等于内外复杂度乘积
int cal(int n){
int ret = 0;
int i = 1;
for(; i < n; ++ i){
ret = ret + f(i);
}
}
int f(int n){
int sum = 0;
int i = 1;
for(; i < n; ++ i){
sum = sum + i;
}
return sum;
}
T(n) = O(n ^ 2)
4.常见的复杂度量级
复杂度量级分为两类:
多项式时间复杂度
- 常数级时间复杂度 O(1)
只要代码数据规模不随n的增长变化
- 对数级时间复杂度 O(log n)
- 线性阶 O(n)
- O(m + n)
由于m和n的数据规模不定,因此使用加法原则保留两者的复杂度
- 线性对数阶 O(nlog n)
- 平方阶 O(n ^ 2)
- 立方阶 O(n ^ 3)
- k次方阶 O(n ^ k)
- 常数级时间复杂度 O(1)
非多项式时间复杂度
- 增数阶 O(2 ^ n)
- 阶乘阶 O(n!)
当n越来越大时,非多项式的时间复杂度急剧增加,因此非多项式的算法基本不用
5.最好、最坏、平均时间复杂度
int find(int[] array, int n, int x){
int i = 0;
int pos = -1;
for(; i < n; ++ i){
if(array[i] == x){
pos = i;
break;
}
}
return pos;
}
最好情况:O(1)
最坏情况:O(n)
平均(加权计算,基本和最坏保持一致):O(n)
总结:O(1) < O(log n) < O(n) < O(nlog n) < O(n ^ 2)
6.空间复杂度
一个算法在运行过程中临时占用存储空间大小的度量
,一般有如下公式:
S(n) = O(f(n))
- 随代码规模增加占用的空间也正向增加
单次函数调用空间复杂度就是O(1),递归调用空间复杂度就是O(n)