0x00 复杂度分析
数据结构和算法本身解决的是‘快’和‘省’的问题,即如何让代码运行得更快,如何让代码更省存储空间,所以如何衡量算法的执行效率就至关重要,这就是我们要学习的时间、空间复杂度分析
0x01 为什么需要复杂度分析
通过代码时长来判断效率的方法有一定的缺陷
- 测试结果非常依赖测试环境
- 测试结果受数据规模的影响很大
我们需要一个不用具体的测试数据来测试,就可以粗略地估计算法的执行效率的方法
大o复杂度表示法
所有代码的执行时间T(n)与每行代码的执行次数成正比
T(n) = O(f(n))
- T(n)表示代码执行时间
- n表示数据规模大小
- f(n)表示每行代码执行的次数总和
- O表示代码的执行时间T(n)和f(n)表达式成正比
大O时间复杂度实际上并不表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以也叫渐进时间复杂度,简称时间复杂度
0x02 时间复杂度分析
- 只关注循环执行次数最多的一段代码
- 加法法则:总复杂度等于量级最大的那段代码的复杂度
如果 T1(n)=O(f(n)),T2(n)=O(g(n))
那么 T(n)=T1(n)+T2(n)=max(O(f(n)),O(g(n))) = O(max(f(n),g(n)))
- 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
如果 T1(n)=O(n),T2(n)=O(n2);那么T(n)=T1(n)*T2(n)=O(n)*O(n2)=O(n^3)
在采用大O标记复杂度的时候,可以忽略系数,即O(Cf(n)) = O(f(n))
常见时间复杂度分析
非多项式量级
指数阶:O(2^n)
阶乘阶:O(n!)
多项式量级
常数阶:O(1)
对数阶:O(logn)
线性阶:O(n)
线性对数阶:O(nlogn)
平方阶:O(n^2)
立方阶:O(n^3)
k次方阶:O(n^k)
0x03 空间复杂度分析
空间复杂度全称是渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系
常见的空间复杂度是O(1)、O(n)、O(n^2)
0x04 高阶分析
最好最坏情况时间复杂度
最好情况时间复杂度就是,在最理想情况下,代码执行的时间复杂度
最坏情况时间复杂度就是,在最糟糕情况下,代码执行的时间复杂度
平均情况时间复杂度
将各种情况出现的概率乘以执行的时间,累加起来,就是平均时间复杂度
// 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,找到就返回,我们假设x在数组中和不在数组中的概率都是1/2,如果在数组中,在每个位置都是1/n,所以
代码执行1次循环的概率是1/2n,执行2次循环的概率也是1/2n,执行n次的概率是1/2(x不在数组中),累加起来就是平均时间复杂度了
均摊时间复杂度
均摊时间复杂度就是一种特殊的平均时间复杂度
对一个数据结构进行一组连续操作中,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,而且这些操作之间存在时序关系,这时候,可以将一组操作放在一块分析,看能否将较高时间复杂度操作的那次耗时平摊到其他复杂度较低的操作上
0x05 主定理
以上是极客时间专栏的复杂度学习笔记,老师没有详细讲主定理,这里查询资料,将笔记做到一起
主定理用来分析许多由分治法得到的递推关系式的方法
T(n) = aT(n/b) + f(n)
- a表示递推的子问题数量
- n/b表示每个子问题的规模
- f(n)表示递推以外进行的计算工作
a≥1,b>1为常数,f(n) 为函数,T(n) 为非负整数。则有以下结果(分类讨论):
总结下来分三种情况:
这里的比较不是真正意义上的大与小于,而是多项式意义上的大于小于
对于主定理,有个理解就行了,具体的证明不用完全掌握,详细可参考《算法导论》第三版4.5以及算法导论主定理