时间、空间复杂度分析
总结自:数据结构与算法之美
为什么需要复杂度分析
事后统计法有非常大的局限性。
1.测试结果非常依赖测试环境
硬件的不同对测试结果有很大的影响。
2.测试结果受数据规模的影响很大
排序算法依赖待排数据的有序度。此外,测试数据规模太小,测试结果可能无法真实地反映算法的性能。
大 O 复杂度表示法
【示例】求1,2,3……n的累加和。
int cal(int n) {
int sum = 0;
int i = 1;
for (; i <= n; ++i) {
sum = sum + i;
}
return sum;
}
解题思路:
假设每行代码执行时间都一样,为unit_time
第4 5行都运行了 n 遍,所以需要 2n*unit_time的执行时间。
所以复杂度为 O(n)。
结论:
尽管不知道unit_time的具体值,但通过推到,可以得到一个规律:所有代码的执行时间T(n)与每行代码的执行次数f(n)成正比
总结为一个公式:
具体解释:
T(n)表示代码执行的时间;
n表示数据规模的大小
f(n)表示每行代码执行的次数总和
所以,第一个例子中 T(n) = O(2n+2)。而时间复杂度实际上表示代码执行时间随数据规模增长的变化趋势。这里其实引用的是极限的思想,当n趋近于无限大时,常量、低阶、系数对数据的影响无限趋近于0,所以省略这部分。得到最终的时间复杂度。
时间复杂度分析
1.只关注循环执行次数最多的一段代码
刚刚已经说了我们通常会忽略掉公式中的常量、低阶、系数,只需要记录一个最大阶的量级就可以了。所以我们在分析一个算法、一段代码的时间复杂度的时候,也只关注循环执行次数最多的那一段代码就可以了
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 < n; ++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;
}
思路分析
代码分为3部分,分别是求 sum_1, sum_2, sum_3。分别分析三段的时间复杂度。再取一个量级最大的作为整个代码的时间复杂度。
- sum_1:代码总共执行100次,属于常量级,可以忽略。
- sum_2: O(n)
- sum_3: O(n^2)
那么整段代码的时间复杂度就是 O(n^2)
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;
}
思路分析
如果 T1(n)=O(f(n)),T2(n)=O(g(n));那么T(n)=T1(n)T2(n)=O(f(n))O(g(n))=O(f(n)*g(n)).
回到代码上,f()方法的时间复杂度为 O(n);4~6行代码的时间复杂度为 O(n)。那么整个cal()的时间复杂度为 O(n^2)
几种常见的时间复杂度
基本概念:
粗略的将复杂度量级分为多项式量级和非多项式量级。非多项式量级只有两个:O(2^n)和O(n!)
1.O(1)
一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)。
【示例】
int i = 8;
int j = 6;
int sum = i + j;
2.O(logn)、O(nlogn)
【示例】
i=1;
while (i <= n) {
i = i * 2;
}
思路分析
变量 i 的值从 1 开始取,每循环一次就乘以 2。当大于 n 时,循环结束。
变量 i 的取值就是一个等比数列
如图,通过 2x=n 求解 x。得出 x = log2n
实际上,不管是以 2 为底、以 3 为底,还是以 10 为底,我们可以把所有对数阶的时间复杂度都记为 O(logn)。
比如,log3n = log32 * log2n 而 log32 是一个常量,因此以几为底无所谓。
理解了 O(logn),那么 O(nlogn) 实际上就是把复杂度为 O(logn) 的代码执行了 n 遍。
3.O(m+n)、O(m*n)
【示例】
int cal(int m, int n) {
int sum_1 = 0;
int i = 1;
for (; i < m; ++i) {
sum_1 = sum_1 + i;
}
int sum_2 = 0;
int j = 1;
for (; j < n; ++j) {
sum_2 = sum_2 + j;
}
return sum_1 + sum_2;
}
从代码中可以看出,m 和 n 是表示两个数据规模。我们无法事先评估 m 和 n 谁的量级大,所以我们在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。所以,上面代码的时间复杂度就是 O(m+n)。
根据乘法法则,T1(m)*T2(n) = O(f(m) * f(n))。
空间复杂度分析
空间复杂度全称就是渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系。
【示例】
void print(int n) {
int i = 0;
int[] a = new int[n];
for (i; i <n; ++i) {
a[i] = i * i;
}
for (i = n-1; i >= 0; --i) {
print out a[i]
}
}
思路分析
第 3 行申请了一个大小为 n 的 int 类型数组,除此之外,剩下的代码都没有占用更多的空间,所以整段代码的空间复杂度就是 O(n)。
最好、最坏时间复杂度
【示例】
// n表示数组array的长度
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;
}
最好情况:第一个元素是要查找的变量 x。时间复杂度 O(1)
最坏情况:最后一个元素是要查找的变量 x,或者数组中没有变量 x。时间复杂度 O(n)。
最好时间复杂度:在最理想的情况下,执行这段代码的时间复杂度。
最坏时间复杂度:在最糟糕的情况下,执行这段代码的时间复杂度。
平均时间复杂度
继续分析上个示例
- 数组中存在或者不存在 x 的概率都为 1/2。
- 数组中查找任意位置出现 x 的概率为 1/n。
根据乘法法则,查找数据出现在 0~n-1任意位置的概率为 1/(2n)。平均时间复杂度计算:
这个值就是概率论中的加权平均值,也叫作期望值,所以平均时间复杂度的全称应该叫加权平均时间复杂度或者期望时间复杂度。