重温:数据结构与算法 - 开篇
重温:数据结构与算法 - 复杂度分析(一)
前章,学习了
- 什么是大O复杂度分析
- 有哪些复杂度分析技巧
- 什么是时间复杂度
- 什么是空间复杂度
- 有哪些常见的复杂度:O(1)、O(logn)、O(n)、O(nlogn)
如果对以上提到的内容还不了解,可以点击上方的链接查看;
本章将介绍几种特殊场景下的时间复杂度,以及分析方法。
最好、最坏情况时间复杂度
int find(int[] arr, int x) {
int position = -1;
int n = arr.length;
for (int i = 0; i < n; i++) {
if (arr[i] == x) {
position = i;
break;
}
}
return position;
}
这是一个从数组arr中查询x的索引位置的方法,如果不存在返回-1。我们尝试使用之前的方法先标注出每行代码的单位时间:
int position = -1; // 1 * unitTime
int n = arr.length; // 1 * unitTime
for (int i = 0; i < n; i++) { // ???
if (arr[i] == x) { // ???
position = i; // ???
break; // ???
}
}
我们发现循环体中的代码执行次数是不确定的:
- 当x在数组索引0的位置,那么它的时间复杂度是:O(1);
- 当x不存在数组中,时间复杂度又变成了:O(n).
这时就产生了两个极端情况下的时间复杂度,称之为:最好情况时间复杂度 与 最坏情况时间复杂度。
(加权)平均情况时间复杂度
既然有最好情况、最坏情况,那么在两者之间肯定有很多我们叫不上名的情况,有没有一种统一的方式表示这个算法的复杂度呢?
通过分析不难发现:随着x在的arr数组中索引的不同需要的循环次数也就不同。
我们尝试把所有可能执行的次数累加出来,并除以总的情况次数,来计算平均每种情况的循环次数。
索引位置 | 执行次数 |
---|---|
0 | 1 |
1 | 2 |
2 | 3 |
... | ... |
n-1 | n |
不存在 | n |
循环总次数 = 1+2+3+...+n+n
总情况数 = n+1
计算: f(n) = (1+2+3+...+n+n) / (n+1)
1+2+3+...+n 中学数学里的等差数列还有印象吗?我们套入等差数列求和公式:
等差数列求和公式.png循环总次数 = 1+2+3+...+n+n = n * (n+1) / 2 + n = n(n+3)/2 = ( n² +3n ) / 2
则: f(n) = ( n² +3n ) / 2 / (n+1) = (n²+3n) / (2n+2)
套用复杂度分析法,去掉系数:
f(n) = ( n²+n ) / n = n+1 = n最终:T(n) = O(n)
通过求平均数并且套用大O复杂度表示法,我们得到这个例子的平均时间复杂度为:O(n).
然而,这个平均情况时间复杂度我们在推算过程中并不完全正确,我们忘了什么呢?
试想一下,循环总次数除以总情况数表示的是所有情况出现的概率一致时的平均时间复杂度,那每种情况出现的概率真的都一致吗?
这里就需要引入一点高中数学概率论的知识了:
简单解释下,虽然我们已经统计出来会出现n+1种情况,但每种情况出现的概率其实是不同的:
x存在或者不存在arr数组中的概率简单看作1/2;
x存在arr数组中时,出现在索引某个位置的概率又是1/n;
那么x存在arr数组中的某个索引的概率其实是1/2n。
那么我们把概率重新套入到之前的推算过程中:
x在arr数组中情况循环次数 = (1+2+3+...+n)/2n = (n+1)/4
x不在arr数组中情况循环次数加入概率:n/2
套入: f(n)= (n+1)/4 + n/2 = (3n+1)/4
去掉常量与系数后:T(n) = O(n)
最终得到的这个值,在概率论中称之为加权平均值,这种时间复杂度称之为:加权平均时间复杂度。
总结下,同一段代码,在不同的情况下出现多种不同的时间复杂度,这时候我们会用最好时间复杂度、最坏时间复杂度、(加权)平均时间复杂度来表示。
均摊时间复杂度
到这里复杂度分析的内容基本总结完了,而现在要学的内容可以看作为平均时间复杂度的一种特殊情况。
class IntArr {
private final int DEF_SIZE = 10;
private int[] arr = new int[DEF_SIZE];
private int len = DEF_SIZE;
private int index = 0;
public void add(int num) {
if (index >= len) {
//空间不足,扩充数组大小,比之前多一半
int newLen = len + len / 2;
int[] newArr = new int[newLen];
for (int i = 0; i < len; i++) {
newArr[i] = arr[i];
}
arr = newArr;
len = newLen;
}
arr[index] = num;
index++;
}
...
}
上面例子是一个整形数组容器,它的好处是可以自动扩展空间大小,我们尝试计算出它的时间复杂度。
通过分析:
- 每当数组大小不够用时,会扩展之前大小的一半,并且copy旧数据
分析过程:
第一遍:
当 index < len 时,时间复杂度是 O(1)
当 index >= len 时,时间复杂度是O(n)
扩展后第二遍:
当 index < len 时,时间复杂度是 O(1)
当 index >= len 时,时间复杂度是O(n)
扩展后第三遍:
...
扩展后第N遍:
当 index < len 时,时间复杂度是 O(1)
当 index >= len 时,时间复杂度是O(n)
很明显,它的最好时间复杂度是O(1),最坏时间复杂度是O(n),那它的平均时间复杂度是多少呢?
假设当前数组长度是n,填加一个数据进入数组分两种情况:
- 当前添加位置小于n,那么所需执行次数为1,这种情况下的总总次数是1+1+1+...+1=n;
- 当添加的位置大于等于n时,所需的执行次数为n。
总次数就为:1+1+1+...+1+n = 2n
无论是小于n里的每一次情况还是大于等于n时的情况,它们的发生的概率都是1/(n+1)
f(n) = 2n/(n+1) = 1
则最终加权平均时间复杂度为:O(1)
我们通过概率的形式算出了加权平均时间复杂度,当然这个算法是肯定没问题的。不过,其实有更简单的方法算出它的平均时间复杂度:
通过上面每一遍分析,可以得到规律:
- 每出现n次O(1)复杂度后,会再出现一次O(n)复杂度
我们假设把O(n)复杂度分为n份,每份的执行次数也就成了1,把这些拆分的单位次数平摊到前面的n次O(1)中,尽管前面的n次O(1) 尽管执行次数变成了2,但它的时间复杂度依旧是O(1)。
上面这种分析方法,称为平摊分析法,分析出的时间复杂度称为均摊时间复杂度。
对于这种随着数据的不断改变,出现了某种固定规律的算法,我们可以尝试使用平摊分析法快速得到均摊时间复杂度。
小结
到此,复杂度分析就全部学完,这节学习了:
- 最好情况时间复杂度
- 最坏情况时间复杂
- (加权)平均时间复杂度
- 均摊时间复杂度
要想学好数据结构与算法,复杂度分析就是重中之重,学会复杂度分析就相当于学会了一半。
复杂度分析并不难,贵在练习!
网友评论