2. 复杂度分析
2.1 什么是复杂度分析
数据结构和算法的本质:快和省,如何让代码运行得更快、更省存储空间。
算法复杂度分为时间复杂度和空间复杂度,从执行时间和占用空间两个维度来评估数据结构和算法的性能。
复杂度描述的是算法执行时间(或占用空间)与数据规模的增长关系,越高阶复杂度的算法,执行效率越低。
2.2 为什么需要复杂度分析
如果采用性能测试,测试结果非常依赖测试环境和受数据规模的影响很大。而复杂度分析有不依赖执行环境、成本低、效率高、易操作、指导性强的特点。
2.3 如何进行复杂度分析
2.3.1 大O表示法
不是代码真正执行的时间,而是表示代码执行时间随数据规模增长的变化趋势,叫作渐进时间复杂度,简称时间复杂度。
表示代码执行的时间; 表示数据规模的大小; 表示每行代码执行的次数总和; 表示代码的执行时间 与 表达式成正比。
2.3.2 时间复杂度
- 只关注循环执行次数最多的一段代码
- 加法规则:总复杂度等于量级最大的那段代码的复杂度
- 乘法规则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
- 代码复杂度由两个数据规模决定的,不能简单利用加法规则,复杂度为
几种常见时间复杂度:
-
多项式量级:
- 常量阶 :代码执行时间不随n的增大而增长
- 对数阶
- 线性阶
- 线性对数阶
- 平方阶 、立方阶 、 k次方阶
-
非多项式量级:当数据规模越来越大时,算法执行时间会急剧增加,非常低效
- 指数阶
- 阶乘阶
2.3.3 空间复杂度
渐进空间复杂度,表示算法的存储空间与数据规则之间的增长关系。
常见的空间复杂度:
2.4 时间复杂度情况
-
最好情况时间复杂度(best case time complexity)
-
最坏情况时间复杂度(worse case time complexity)
-
平均情况时间复杂度(average case time complexity)
-
均摊时间复杂度(amortized time complexity)
2.4.1 最好情况时间复杂度
在最理想的情况下,执行这段代码的时间复杂度。
以下面这段代码为例,
// 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正好是数组的第一个元素,对应的时间复杂度即为最好情况时间复杂度。
2.4.2 最坏情况时间复杂度
在最糟糕的情况下,执行这段代码的时间复杂度。
以刚刚举的例子,如果数组中没有要查找的变量x,需要把整个数组都遍历一遍时,此时对应的时间复杂度即为最坏情况时间复杂度。
2.4.3 平均情况时间复杂度
也叫加权平均时间复杂度或期望时间复杂度,用代码在所有情况下执行的次数的加权平均值表示。
假设在数组中与不在数组中的概率都为 ,且要查找的数据出现在 0~n-1 这 n 个位置的概率也是一样的,都为 。因此,根据概率乘法法则,要查找的数据出现在 0~n-1 中任意位置的概率是 。那么平均情况复杂度的计算过程为:
2.4.4 均摊时间复杂度
对一个数据结构进行一组连续操作中,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,而且这些操作出现的频率是非常有规律的,之间存在前后连贯的时序关系。此时,就可以将这一组操作一起分析,将较高时间复杂度的那次操作的耗时,平摊到其他那些时间复杂度比较低的操作上。能够应用均摊时间复杂度分析的场合,一般均摊时间复杂度等于最好情况时间复杂度。
均摊时间复杂度应用的场景特殊、有限,可以看成一种特殊的平均时间复杂度。
2.4.5 总结
只有同一块代码在不同的情况下,时间复杂度有量级的差距,我们才会使用这几种复杂度表示法来区别。一般情况下,使用一个复杂度就可以满足需求。