上一篇介绍了复杂度分析
但是,同一段代码在不同的输入情况下,复杂度量级有可能是不一样的
例如:在大小为n的数组中查找一个数值,
那么这个数有可能出现在数组的第1个位置,也有可能出现在数组的最后一个位置
他们时间复杂度分别是O(1)和O(n)
所以需要引入另外4个时间复杂度分析:
最好情况时间复杂度
最坏情况时间复杂度
平均情况时间复杂度
均摊时间复杂度
其中,最好,最坏情况时间复杂度比较简单
最好情况时间复杂度:在最理想的情况下,执行这段代码的时间复杂度
最坏情况时间复杂度:在最糟糕的情况下,执行这段代码的时间复杂度
从一个无序数组array中,查找元素值为x的位置,若没找到则返回-1
/**
* 从数组中查找值为val的元素下标
* @param array 无序数组
* @param val 数组中的元素
* @return pos 数组中值为val的下标
*/
public int find(int[] array, int val) {
int pos = -1;
if (array != null && array.length > 0){
for (int i = 0; i < array.length; i++) {
if (array[i] == val) {
pos = i;
}
}
}
return pos;
}
由于需要在循环中进行比较,所以代码的时间复杂度是O(n)
实际上,在数组中查找一个元素并不一定需要遍历整个数组
如果在数组遍历的中途就已经找到了,那么循环可以提前结束
/**
* 从数组中查找值为val的元素下标
*
* @param array 无序数组
* @param val 数组中的元素
* @return pos 数组中值为val的下标
*/
public int find(int[] array, int val) {
int pos = -1;
if (array != null && array.length > 0){
for (int i = 0; i < array.length; i++) {
if (array[i] == val) {
pos = i;
break;
}
}
}
return pos;
}
找到值为val的元素后,添加break停止继续
那么:
最好的情况是数组第一个元素就是我们要找的元素,此时时间复杂度为O(1)
最坏的情况是数组中不存在值为x的元素或这个元素在数组的最后,此时时间复杂度为O(n)
所以,同样的一段代码在不同情况下,复杂度量级有可能是不一样
最好,最坏情况时间复杂度都是极端情况下的代码复杂度,发生概率较小
所以,为了表示代码在平均情况下的时间复杂度,引入了平均时间复杂度概念
平均情况时间复杂度的分析涉及概率统计
1)值为val的元素有可能存在数组array中,也有可能不在数组中,所以共n+1种可能
2)若元素存在数组中,则可能存在0~n-1任何一个位置,每个位置的概率均为1/n
3)若元素在数组中和不在数组中两种可能各为50%,
那么,要查找元素出现在n~n-1中任何位置的概率为1/2 * 1/n = 1/(2n)
4)元素位于0~n-1每种情况下需要遍历的元素数量个数为:1,2,3,...,n,n
5)元素在0~n-1位置出现的每一种概率及需要遍历元素的个数总和除以n+1种可能
1*1/(2n) + 2*1/(2n) + 3*1/(2n) + …+ n*1/(2n) + n*1/2
= (3n + 1) / 4
这个值就是概率中的加权平均值,也叫作期望值
用大O表示法,去掉系数,低阶和常量,加权平均时间复杂度为O(n)
平均时间复杂度全称叫做加权平均时间复杂度,或期望时间复杂度
大多数情况下,我们并不需要区分最好,最坏,平均情况时间复杂度
只有当同一段代码在不同情况下,时间复杂度有量级差距时,
才会使用这三种复杂度表示法来区分
除了最好,最坏,平均情况时间复杂度外,
在特殊情况下,还有摊时间复杂度,及对应的摊还分析法,(也叫平摊分析法)
比较典型的一个例子就是向数组中插入数据:
int count = 0;
int[] array = new int[n];
/**
* 向数组中插入数据
*/
public void insert(int val) {
// 空间不足时,先sum放入数组开头
if (count == array.length) {
int sum = array[0];
for (int i = 1; i < array.length; i++) {
sum = sum + array[i];
}
array[0] = sum; // array[0]存储累加结果
count = 1; // 从array[1]开始存数据
}
// 有空间直接添加
array[count] = val;
++count;
}
向数组中添加数据:
当空间不足时,对除array[0]以外数据求和,累计存入array[0]
当空间充足时,直接将数据添加至数组中
最好情况时间复杂度:
向数组添加数据,数组有可用空间直接插入,此时时间复杂度为O(1)
最坏情况时间复杂度:
向数组添加数据,数组已经满了,这时需要对数组遍历求和,时间复杂度为O(n)
平均情况时间复杂度:
假设数组长度为n
那么插入数据位置有n-1种情况,每种情况时间复杂度为O(1)
如果数组没有空间了,需要遍历数组求和,时间复杂度为O(n)
假设以上n种情况发生的概率相同,都是1/n
根据加权平均计算方法,求平均时间复杂度:
1*1/n + 1*1/n + 1*1/n + …+ 1*1/n + n*1/n = (n-1)/n + 1 = O(1)
使用摊还分析法求均摊时间复杂度:
在上边的例子中不难发现:
每向数组中添加n-1个数据后,数据会遍历求和一次
出现的频率是非常有规律的,而且有一定的前后时序关系
也就可以认为:每经过n-1个O(1)时间复杂度,出现一次O(n)时间复杂度
这种特殊场景的复杂度分析,无需找出所有输入情况及对应的发生概率,再计算加权平均值
将耗时较多的那次操作,均摊到接下来n-1次耗时较少的操作上,
那么,均摊下来这一组连续操作的均摊时间复杂度就是O(1)
均摊时间复杂度和摊还分析法的应用场景比较特殊,大致如下:
对数据结构进行一组连续操作,大部分情况时间复杂度很低,个别情况时间复杂度较高,
连续的操作间存在明确的先后时序关系,
此时可以考虑将时间复杂度较高的操作,平摊到其他时间复杂度较低的操作上
均摊时间复杂度就是一种特殊的平均时间复杂度
在能够使用的场景中,一般均摊时间复杂度就等于最好情况时间复杂度
数据结构和算法解决的是快和省的问题
复杂度分析表示的是随数据量级增加,代码执行效率和空间占用的变化趋势
当同一段代码在不同输入下,存在复杂度量级改变时,将继续评估其复杂度
四种时间复杂度:
最好情况时间复杂度
最坏情况时间复杂度
平均情况时间复杂度
均摊时间复杂度(摊还分析法)