写在前面:
本系列文章只作为自己学习数据与结构算法这门课过程的记录,如果有不对的地方,欢迎读者指出,大家一起学习进步。
(注:文中图片来源于极客时间课程:《数据结构与算法之美》)
为什么要进行复杂度分析?
数据结构和算法解决是“如何让计算机更快时间、更省空间的解决问题”。时间复杂度和空间复杂度分别从时间、空间两个维度来衡量数据结构和算法的性能,二者被统称为复杂度。复杂度描述的是算法执行时间/数据结构占用空间与数据规模增长的关系。
那么为什么要进行复杂度分析呢?
- 与性能测试相比,复杂度分析不依赖测试环境,有效率高、指导性强的特点。
- 性能测试的测试结果受数据规模的影响很大,复杂度分析不需要具体的测试数据来测试,就可以粗略的估计算法的执行效率。
如何进行复杂度分析?
- 大O表示法:代码的执行时间与每行代码的执行次数成正比关系,可以表示为T(n) = O(f(n)),其中T(n)表示算法执行总时间,f(n)表示每行代码执行总次数,n表示数据的规模。
- 复杂度分析法则:
(1)多段循环取最大:一段代码中包含多段循环,取循环规模最大的代码段。
(2)嵌套循环取乘积:如递归、多重循环等。
(3)多个规模求加法:多个参数影响的多个数据规模,取几个循环的复杂度相加。 - 最坏情况时间复杂度:代码在最理想情况下执行的时间复杂度。
- 最好情况时间复杂度:代码在最坏情况下执行的时间复杂度。
- 平均时间复杂度:用代码在所有情况下执行的次数的加权平均值表示。
举例:要查找的变量 x 在数组中的位置。
我们知道,要查找的变量 x,要么在数组里,要么就不在数组里。这两种情况对应的概率统计起来很麻烦,为了方便你理解,我们假设在数组中与不在数组中的概率都为 1/2。另外,要查找的数据出现在 0~n-1 这 n 个位置的概率也是一样的,为 1/n。所以,根据概率乘法法则,要查找的数据出现在 0~n-1 中任意位置的概率就是 1/(2n)。
- 均摊时间复杂度:在代码执行的所有复杂度情况中绝大部分是低级别的复杂度,个别情况是高级别复杂度且发生具有时序关系时,可以将个别高级别复杂度均摊到低级别复杂度上。基本上均摊结果就等于低级别复杂度。
下面举一个均摊时间复杂度的例子。
// array表示一个长度为n的数组
// 代码中的array.length就等于n
int[] array = new int[n];
int count = 0;
void insert(int val) {
if (count == array.length) {
int sum = 0;
for (int i = 0; i < array.length; ++i) {
sum = sum + array[i];
}
array[0] = sum;
count = 1;
}
array[count] = val;
++count;
}
这段代码实现了一个往数组中插入数据的功能。当数组满了之后,也就是代码中的 count == array.length 时,我们用 for 循环遍历数组求和,并清空数组,将求和之后的 sum 值放到数组的第一个位置,然后再将新的数据插入。但如果数组一开始就有空闲空间,则直接将数据插入数组。
最理想的情况下,数组中有空闲空间,我们只需要将数据插入到数组下标为 count 的位置就可以了,所以最好情况时间复杂度为 O(1)。最坏的情况下,数组中没有空闲空间了,我们需要先做一次数组的遍历求和,然后再将数据插入,所以最坏情况时间复杂度为 O(n)。那平均时间复杂度是多少呢?答案是 O(1)。我们还是可以通过前面讲的概率论的方法来分析。假设数组的长度是 n,根据数据插入的位置的不同,我们可以分为 n 种情况,每种情况的时间复杂度是 O(1)。除此之外,还有一种“额外”的情况,就是在数组没有空闲空间时插入一个数据,这个时候的时间复杂度是 O(n)。而且,这 n+1 种情况发生的概率一样,都是 1/(n+1)。所以,根据加权平均的计算方法,我们求得的平均时间复杂度就是:
常见的复杂度量级
- 多项式量级:随着数据规模的增长,算法的执行时间和空间占用,按照多项式的比例增长。包括:O(1)(常数阶)、O(logn)(对数阶)、O(n)(线性阶)、O(nlogn)(线性对数阶)、O(n2)(平方阶)、O(n3)(立方阶)
- 非多项式量级:随着数据规模的增长,算法的执行时间和空间占用暴增,这类算法性能极差。包括:O(2n)(指数阶)、O(n!)(阶乘阶)
- 常见的复杂度排序:
从低阶到高阶有::O(1)、O(logn)、O(n)、O(nlogn)、O(nn)。
- 常见的空间复杂度:O(1)、O(n)、O(n2)。
引用课程后的评论区的一个例子:存储一个二进制数,输入规模(空间复杂度)是O(logn) bit。解释:比如8用二进制表示就是3+1个bit(8用二进制表示是:1000)。16用二进制表示就是4+1个bit(16用二进制表示是:10000)。以此类推 n用二进制表示就是(logn) +1个bit