为什么需要复杂度分析?
有些人认为写的一套算法,通过监控和统计,就能很准确的知道运行时间和占用的内存大小。 这种统计法是对的,但是它有一个很大的局限。
结果依赖于机器
找配置高的机器,和配置低的机器,执行结果是不一样的。
测试数据的规模影响测试结果
数据的规模很大概率影响着算法的执行时间和结果。
规律总结
所有代码的执行时间 T(n) 与每行代码的执行次数 f(n) 成正比。
总结成为一个公式
- T(n)表示代码的总执行时间。
- n表示数据的规模。
- f(n) 表示每行代码执行的次数总和
- 公式中的 O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比。
时间复杂度的由来
时间复杂度例子1:T(n) = O(2n+2), 例子2:T(n) = O(2n2+2n+3), 大O复杂度表示的不是代码真正的执行时间,而是随着数据规模的增加,时间增长的趋势。所以也称渐进时间复杂度。简言之,就是时间复杂度。
当 n 很大时,你可以把它想象成 10000、100000。而公式中的低阶、常量、系数三部分并不左右增长趋势,所以都可以忽略。我们只需要记录一个最大量级就可以了,如果用大 O 表示法表示刚讲的那两段例子的时间复杂度,就可以记为:T(n) = O(n); T(n) = O(n^2)。
如何进行时间复杂度的分析?
1.只关注执行次数最多的一段代码。
大O复杂度表示的是一种随着数据规模,时间的变化趋势。我们通常会忽略掉公式中的常量、低阶、系数,只需要记录一个最大阶的量级就可以了。
int cal(int n) {
int sum = 0;
int i = 1;
for (; i <= n; ++i) {
sum = sum + i;
}
return sum;
}
第二行、第三行代码是常量级的执行时间,与n的大小无关,对复杂度没有影响。执行最多的是这个循环,执行了n次,所以总的时间复杂度就是 O(n)。
2.加法法则,总的复杂度等于量级最大那段代码的复杂度。
要点:常量级的执行时间,跟复杂度无关。即使某个循环执行了一千次、一万次,只要是一个已知的数,那它就是常量级的执行时间。虽然n这个数字很大,对执行时间有很大的影响。但是回到时间复杂度这个概念来说,它表示算法执行效率随数据规模增长变化的趋势。所以不管执行时间多长,我们都可以忽略掉,因为它本身对增长趋势没有影响。
3. 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
其实,这个好理解,我们理解为嵌套循环就好。例如一个循环内,调用了另外一个循环,那这个代码的复杂度,就是复杂度的乘积。如果 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)).
几种常见的时间复杂度
空间复杂度的由来
举一反三,时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。类比一下,空间复杂度全称就是渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系。
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]
}
}
我们定义了一个常量int i =0;以及一个长度为n的数组,所以整段代码的空间复杂度为O(n)。
空间复杂度比时间复杂度简单很多,我们常见的空间复杂度就是 O(1)、O(n)、O(n2 ),像 O(logn)、O(nlogn) 这样的对数阶复杂度平时都用不到。
总结
复杂度包含时间复杂度和空间复杂度,我们常见的时间复杂度如上图所示,熟练的掌握复杂度分析,使我们学习算法的一项必备技能。