数据结构与算法学习-复杂度分析

前言

这一篇笔记主要记录总结了什么是算法复杂度?、为什要做算法复杂度分析?、如何做算法复杂度分析?、常用的复杂度级别?以及如何掌握复杂度分析?等问题。

算法复杂度分析是什么?

数据结构与算法解决的是如何更省、更快的存储和处理数据的问题。因此就需要一个考量效率和资源消耗的方法,这就是复杂度分析方法。

复杂度也叫渐进复杂度,其中包含时间复杂度空间复杂度两个维度,是用来分析算法执行时间(或者占用空间)数据规模的增长关系,可以粗略的表示,越高阶复杂度的算法,执行效率越低。

为什么要做算法复杂度分析?

  1. 统计、监控等事后统计法的局限性:

    1. 测试结果高度依赖测试环境
    2. 测试结果受数据规模影响很大
  2. 因此需要一个不用具体的测试数据就可以粗略地估计算法的执行效率的方法。复杂度分析不依赖执行环境、成本低、可操作性强。

  3. 掌握复杂度分析,将能写出性能更优的代码。

如何做算法复杂度分析?

大 O 复杂度表示法

  • 所有代码的执行时间 T(n) 与每行代码的执行次数 n 成正比。
  • 大 O 时间复杂度实际上并不具体代表代码真正的执行时间,而是表示代码执行时间随数据规模增长变化趋势,叫做渐进时间复杂度,简称时间复杂度
  • 由于公式中的低阶、常量、系数对增长趋势不影响,所以都可以忽略,只需要记录一个最大的量级。

算法复杂度分析法则

  1. 只关注循环执行次数最多的一段代码
  2. 加法法则:总复杂度等于量级最大的那段代码的复杂度
  3. 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

空间复杂度分析

类比时间复杂度,空间复杂度全称就是渐进空间复杂度,表示算法的存储空间数据规模之间增长关系

常见的空间复杂度是:O(1)、O(n)、O(n^2)

最好、最坏时间复杂度

对有些代码,其复杂度有不是固定的,有最好、最坏的情况。如数组元素查找,有可能第一次就找到,这个时候算法时间复杂度是常数阶O(1),也有可能需要整个遍历一遍数组才能找到,这个时候是最坏的时间复杂度O(n)。

  • 最好时间复杂度:在最理想情况下,代码执行的时间复杂度。
  • 最坏时间复杂度:在最糟糕的情况下,代码执行的时间复杂度。

平均情况复杂度

由于最好和最坏情况复杂度对应的都是极端情况下的代码复杂度,发生的概率并不大,所以需要引入平均情况复杂度来更好的表示算法的时间复杂度。

平均情况复杂度 是将各种情况下时间复杂度发生的概率考虑进去,然后计算整体时间复杂度的期望值,所以平均情况复杂度也叫做加权平均时间复杂度或者期望时间复杂度

很多时候,只要使用一个复杂度就可以满足需求了。只有同一块代码在不同情况下,时间复杂度有量级的差距,才会使用这三种复杂度表示法来区分。

均摊时间复杂度

适用场景:对一个数据结构进行一组连续操作中,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,而且这些操作之间存在前后连贯的时序关系,这个情况下,就可以将这一组操作放在一起分析,看能否将较高时间复杂度的那次操作平摊到其他那些时间复杂度较低的操作上。一般在能够应用均摊时间复杂度的场景,均摊时间复杂度就等于最好情况时间复杂度

分析方法:摊还分析

常用的算法复杂度级别

  • 非多项式量级
    • 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. 分析以下代码的复杂度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 全局变量,大小为 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)。

分享个人技术学习记录和跑步马拉松训练比赛、读书笔记等内容,感兴趣的朋友可以关注我的公众号「青争哥哥」。

0%