复杂度
复杂度也叫渐进复杂度,包括时间复杂度和空间复杂度,用来分析算法执行效率与数据规模之间的增长关系。
复杂度分析可以在初期帮助程序员预估该程序的性能耗费。
时间复杂度用于表示算法的时间耗费与数据规模增长之间的关系
空间复杂度用于表示算法的存储空间与数据规模增长之间的关系
时间复杂度
规则
-
总复杂度等于量级最大的那段代码的复杂度,比如说一个程序中存在两段不同时间复杂度的代码块:
int f(int n) { int sum = 0; for (int i = 0; i < n; ++i) { sum ++; } for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { sum ++; } } return sum; }
第一个for循环的时间复杂度T1,和第二个嵌套for循环的时间复杂度T2分别为:
T 1 ( n ) = O ( n ) , T 2 ( n ) = O ( n 2 ) T1(n)=O(n),T2(n)=O(n^2) T1(n)=O(n),T2(n)=O(n2)
那么整个程序的时间复杂度为:
T ( n ) = O ( m a x ( n , n 2 ) ) = O ( n 2 ) T(n)=O(max(n, n^2)) = O(n^2) T(n)=O(max(n,n2))=O(n2) -
如果量级大的代码块有多个,而我们无法事先评估谁的量级大,就不能简单地省略其中一个,例如:
int f(int n, int m) { int sum = 0; for (int i = 0; i < n; ++i) { sum ++; } for (int i = 0; i < m; ++i) { sum ++; } return sum; }
该函数的时间复杂度为:
O ( T 1 ( m ) + T 2 ( n ) ) = O ( m + n ) O(T1(m) + T2(n)) = O(m + n) O(T1(m)+T2(n))=O(m+n) -
乘法法则: 嵌套代码的复杂度等于嵌套内外代码复杂度的乘积,例如:
int f(int n, int m) { int sum = 0; for (int i = 0; i < n; ++i) { for (int j = 0; j < m; ++j) { sum += j; } } return sum; }
该函数的时间复杂度为:
T ( n ) = T 1 ( n ) ∗ T 2 ( m ) = O ( n m ) T(n) = T1(n) * T2(m) = O(nm) T(n)=T1(n)∗T2(m)=O(nm)
常见的时间复杂度
常 量 、 线 性 复 杂 度 : O ( 1 ) 、 O ( n ) 常量、线性复杂度:O(1)、O(n) 常量、线性复杂度:O(1)、O(n)
对 数 阶 、 线 性 对 数 阶 复 杂 度 : O ( l o g ( n ) ) 、 O ( n l o g ( n ) ) 对数阶、线性对数阶复杂度:O(log(n))、O(nlog(n)) 对数阶、线性对数阶复杂度:O(log(n))、O(nlog(n))
平 方 、 立 方 … k 次 方 阶 复 杂 度 : O ( n 2 ) 、 O ( n 3 ) … O ( n k ) 平方、立方…k次方阶复杂度:O(n^2)、O(n^3)…O(n^k) 平方、立方…k次方阶复杂度:O(n2)、O(n3)…O(nk)
指 数 阶 、 阶 乘 阶 复 杂 度 : O ( 2 n ) 、 O ( n ! ) 指数阶、阶乘阶复杂度:O(2^n)、O(n!) 指数阶、阶乘阶复杂度:O(2n)、O(n!)
对于常量复杂度而言,执行时间不随 n 的增大而增长的代码,我们都记作 O(1) 。 一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)
对于对数阶复杂度而言, 不管是以 2 为底、以 3 为底,还是以 10 为底,我们可以把所有对数阶的时间复杂度都记为O(logn)
对于指数阶、阶乘阶复杂度而言, 当数据规模 n 增大,算法的执行时间会急剧增加 ,因此这两类时间复杂度的算法是非常低效的算法不推荐使用
最好、最坏、平均时间复杂度
上述情况是程序必须执行完所有代码得出的时间复杂度。而在很多情况下,我们不需要进行完整的遍历或递归,例如查找一个数组中是否存在5:
int val[n];
bool find() {
for (int i = 0; i < n; i++) {
if (val[i] == 5) {
return true;
}
}
return false;
}
最好情况时间复杂度是最理想情况下执行这段代码的时间复杂度,体现在上述代码中,就是数组第一个元素是5,因此最好情况时间复杂度为O(1)
最坏情况时间复杂度是最糟糕情况下执行这段代码的时间复杂度 ,体现在上述代码中,就是数组中不存在元素5,因此最坏情况时间复杂度为O(n)
平均情况时间复杂度是考虑所有可能发生的情况、概率、以及对应耗费的时间,然后取平均值,体现在上述代码中,就是考虑n+1种情况(5出现在0~n-1位置 和 5没有出现)及其概率、对应耗费的时间,得到:
1
∗
1
2
∗
n
+
2
∗
1
2
∗
n
+
.
.
.
+
n
∗
1
2
∗
n
+
n
∗
1
2
=
3
∗
n
+
1
4
1* \frac{1}{2*n} + 2 *\frac{1}{2*n} + ...+n*\frac{1}{2*n} + n*\frac{1}{2} = \frac{3*n+1}{4}
1∗2∗n1+2∗2∗n1+...+n∗2∗n1+n∗21=43∗n+1
由于时间复杂度可以省略掉系数、低阶、常量,把这个公式简化之后得到的平均时间复杂度就是 O(n)
空间复杂度
规则
规则与时间复杂度的三点类似,不再赘述
我们可以通过空间复杂度,来预估程序所耗费的内存。比如:
const int N = 1000000;
int[] val = new int[N];
int型占四字节,对于O(n)空间复杂度的程序来说,当n为1e6时,耗费的内存为:
4
∗
1000000
/
1024
/
1024
≈
4
M
B
4*1000000 / 1024 / 1024 ≈ 4MB
4∗1000000/1024/1024≈4MB
常见的空间复杂度
比较常见的空间复杂度有:
O
(
1
)
、
O
(
n
)
、
O
(
n
2
)
O(1)、O(n)、O(n^2)
O(1)、O(n)、O(n2)
相比时间复杂度, O(logn)、O(nlogn)
等对数阶复杂度平时几乎用不到~