数据结构与算法学习-复杂度分析
前言
这一篇笔记主要记录总结了什么是算法复杂度?、为什要做算法复杂度分析?、如何做算法复杂度分析?、常用的复杂度级别?以及如何掌握复杂度分析?等问题。
算法复杂度分析是什么?
数据结构与算法
解决的是如何更省、更快的存储和处理数据
的问题。因此就需要一个考量效率和资源消耗
的方法,这就是复杂度分析方法。
复杂度也叫渐进复杂度
,其中包含时间复杂度
和空间复杂度
两个维度,是用来分析算法执行时间(或者占用空间)
与数据规模的增长
关系,可以粗略的表示,越高阶复杂度的算法,执行效率越低。
为什么要做算法复杂度分析?
-
统计、监控等
事后统计法
的局限性:- 测试结果高度依赖测试环境
- 测试结果受数据规模影响很大
-
因此需要一个
不用具体的测试数据
就可以粗略地估计算法的执行效率
的方法。复杂度分析
不依赖执行环境、成本低、可操作性强。 -
掌握
复杂度分析
,将能写出性能更优的代码。
如何做算法复杂度分析?
大 O 复杂度表示法
image- 所有代码的执行时间 T(n) 与每行代码的执行次数 n 成正比。
-
大 O 时间复杂度
实际上并不具体代表代码真正的执行时间,而是表示代码执行时间随数据规模增长
的变化趋势
,叫做渐进时间复杂度
,简称时间复杂度
。 - 由于公式中的低阶、常量、系数对增长趋势不影响,所以都可以忽略,只需要记录一个最大的量级。
算法复杂度分析法则
- 只关注循环执行次数最多的一段代码
- 加法法则:总复杂度等于量级最大的那段代码的复杂度
- 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
空间复杂度分析
类比时间复杂度,空间复杂度全称就是渐进空间复杂度
,表示算法的存储空间
与数据规模之间
的增长关系
。
常见的空间复杂度是:O(1)、O(n)、O(n^2)
最好、最坏时间复杂度
对有些代码,其复杂度有不是固定的,有最好、最坏的情况。如数组元素查找,有可能第一次就找到,这个时候算法时间复杂度是常数阶O(1),也有可能需要整个遍历一遍数组才能找到,这个时候是最坏的时间复杂度O(n)。
- 最好时间复杂度:在最理想情况下,代码执行的时间复杂度。
- 最坏时间复杂度:在最糟糕的情况下,代码执行的时间复杂度。
平均情况复杂度
由于最好和最坏情况复杂度对应的都是极端情况下的代码复杂度,发生的概率并不大,所以需要引入平均情况复杂度
来更好的表示算法的时间复杂度。
平均情况复杂度
是将各种情况下时间复杂度发生的概率考虑进去,然后计算整体时间复杂度的期望值,所以平均情况复杂度
也叫做加权平均时间复杂度
或者期望时间复杂度
。
很多时候,只要使用一个复杂度就可以满足需求了。只有同一块代码在不同情况下,时间复杂度有量级的差距
,才会使用这三种复杂度表示法来区分。
均摊时间复杂度
适用场景:对一个数据结构进行一组连续操作中,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,而且这些操作之间存在前后连贯的时序关系
,这个情况下,就可以将这一组操作放在一起分析,看能否将较高时间复杂度的那次操作平摊
到其他那些时间复杂度较低的操作上。一般在能够应用均摊时间复杂度
的场景,均摊时间复杂度
就等于最好情况时间复杂度
。
分析方法:摊还分析
。
常用的算法复杂度级别
image-
非多项式量级
- O(2^n) 和 O(n!)
- 当数据规模 n 越大时,非多项式量级的算法的执行时间会急剧增加。所以非多项式量级算法是非常低效的算法,因此要避免使用到。
-
多项式量级
- O(1):代码的执行时间不随 n 的增大而增长,这样代码的时间复杂度记做 O(1),一般情况下,如果算法中不存在循环语句、递归语句、不管有多少行代码,其时间复杂度都是 O(1)。
- O(logn)、O(nlogn):对数阶复杂度。
- O(m+n)、O(m*n):代码的复杂度由
两个数据的规模
来决定。
-
多项式量级算法时间复杂度随数据规模的增长变化趋势曲线
如何掌握复杂度分析
多练习。
课后问题
1. 项目之前都会进行性能测试,再做代码的时间复杂度、空间复杂度分析是不是多次一举?
解答:不是多此一举,渐进时间,空间复杂度分析为我们提供了一个很好的理论分析的方向,成本低,可操作性强,不用具体的测试数据就可以粗略地估计算法的执行效率,有助于写出性能高效的代码,同时培养起算法复杂度分析的思维,可以和性能测试相辅相成。
2. 分析以下代码的复杂度
// 全局变量,大小为 10 的数组 array,长度 len,下标 i。
int array[] = new int[10];
int len = 10;
int i = 0;
// 往数组中添加一个元素
void add(int element) {
if (i >= len) { // 数组空间不够了
// 重新申请一个 2 倍大小的数组空间
int new_array[] = new int[len*2];
// 把原来 array 数组中的数据依次 copy 到 new_array
for (int j = 0; j < len; ++j) {
new_array[j] = array[j];
}
// new_array 复制给 array,array 现在大小就是 2 倍 len 了
array = new_array;
len = 2 * len;
}
// 将 element 放到下标为 i 的位置,下标 i 加一
array[i] = element;
++i;
}
解答:
- 最好时间复杂度:当 i 小于 len 时,代码不执行 if 语句,算法时间复杂度是 O(1),
- 最坏时间复杂度:当 i >= len 时,代码开始执行 if 语句,里面有个循环操作,所以算法时间复杂度是 O(n)。
- 平均情况复杂度:进入 if 语句后,数组扩容 2 倍,数组每次插入元素的概率都是 1/len + 1,所以根据加权时间复杂度计算公式为 1 * 1/len + 1 + 1 * 1/len + 1 + ... + len *(1/(len+1)) = 1,所以平均情况复杂度是 O(1)。
- 均摊时间复杂度:每次 O(len)前都有 len 次 O(1),存在前后连贯的时序关系,所以可以将高时间复杂度的 O(len) 平摊到之前的 len 次上,所以均摊时间复杂度是 O(1)。
分享个人技术学习记录和跑步马拉松训练比赛、读书笔记等内容,感兴趣的朋友可以关注我的公众号「青争哥哥」。
image