今天,我们继续给大家讲解四个复杂度分析方面的知识点,最好情况时间复杂度、最坏情况时间复杂度、平均情况时间复杂度、均摊时间复杂度。
一、最好、最坏情况时间复杂度
栗子:
//n表示数组array的长度
int find (int[]array,int n,int x){
int i = 0;
int pos = -1;
for (;i<n;++i) {
if (array[i]==x) {
pos=i;
}
}
return pos;
}
我们可以看到,这段代码要实现的功能是,在一个无序的数组(array)中,查找变量x出现的位置。如果没有找到,就返回-1;按照上节课讲的分析方法,这段代码的复杂度为O(n),其中,n代表数组的长度;
我们在数组中查找一个数据,并不需要将整个数组遍历一遍,因为有可能中途找到就可以提前结束循环;因此,我们将上面代码优化一下。
//n表示数组array的长度
int find (int[]array,int n,int x){
int i = 0;
int pos = -1;
for (;i<n;++i) {
if (array[i]==x) {
pos=i;
break;
}
}
return pos;
}
那么,问题就来了,我们优化之后,这段代码的执行时间还是O(n)吗?很显然,上一回我们讲的分析方法,解决不了这个问题。
因为,要查找的变量x可能出现在数组的任意位置。如果数组中第一个元素刚好是需要的元素,那么剩下的n-1次遍历都是多余的,那时间复杂度就是O(1)。如果数组中不存在变量x,那我们需要把数组遍历一遍,时间复杂度就是O(n)。
对于上面这段代码,不同的情况,时间复杂度不一样。为了表示代码在不同情况下的时间复杂度,我们引入了三个概念:最好情况时间复杂度、最坏情况时间复杂度、平均情况时间复杂度。顾名思义,最好情况时间复杂度:在理想情况下,执行代码的时间复杂度;最坏情况时间复杂度:在最糟糕的情况下,执行代码的时间复杂度;
下面我们来看看平均时间复杂度,我们都知道,最好和最坏都是极端情况,发生概率比较小,为了更好地标识平均情况下的时间复杂度,我们引入了:平均情况复杂度。
我们知道要查找的变量x在数组中的位置,有n+1种情况;在数组的0~n-1位置中和不在数组中;我们把每种情况下,查找元素需要遍历的元素个数累加起来,在除以n+1,就可以得到平均值,
根据我们上节讲的,时间复杂度大O标记法中,可以省略掉系数,低阶,常量,所以,刚刚得到的平均时间复杂度就是O(n)。
这个结论是正确的,但是计算方法有一点问题。我们利用概率论的知识重新分析一下。我们知道,要查找的变量x,要么在数组中,要么不在数组中,我们假设在数组中和不在数组中的概率都为1/2。另外,变量x出现在0-n-1这n个位置的概率也是一样的,为1/n;所以,根据概率论乘法法则,要查找的数据出现在任意位置的概率为1/(2n)。
因此,我们前面推理的问题就是没有将各种情况发生的概率考虑进去。所以,现在平均时间复杂度就变成下面这样;
这个值就是概率论中的加权平均值,也叫做期望值。所以平均时间复杂度全称应该叫加权平均时间复杂度或者期望时间复杂度。引入概率之后,这段代码的平均时间复杂度变为(3n+1)/4。去掉系数和常量,仍然是O(n)。
二、均摊时间复杂度
我们先根据下面的栗子来理解一下:
//array表示一个长度为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()时,我们遍历数组求和,并清空数组,将求和之后的sum值放到数组的第一位,然后再将新的数据插入,但如果数组一开始就有空闲空间,则直接将数据插入数组。
【备注】
这里解释一下清空数组:
对于可反复读写的内存空间,count被重置为1,之后再插入的值就能覆盖掉原来的值,相当于将数组清空,并不需要显示的去清空。
首先,我们使用刚刚学习的三种时间复杂度的分析方法来分析一下:
①最理想情况:数组中有空闲空间,我们将数据插入到数组下标为count的位置就可以,那么时间复杂度为O(1)。
②最坏情况:数组中没有空闲空间,我们需要将数组遍历求和,然后再将数据插入,那么时间复杂度为O(n)。
③平均时间复杂度:根据加权平均计算为,为O(1)。
根据上面的学习,我们理解这三种情况应该没有问题,那么有没有分析平均时间复杂度更简单的方法?
我们对比看看find()和insert(),看看两者不同之处;
首先我们发现find()函数在极端情况下,时间复杂度为O(1),但是insert()在大多数情况下,时间复杂度为O(1);
其次,我们发现insert()函数,O(1)时间复杂度和O(n)时间复杂度的插入,出现的很有规律,而且有一定的前后时序关系,一般都是O(n)插入以后,紧跟着n-1个O(1)的插入,循环往复。针对上面的特殊场景的时间复杂度分析,我们引入了一种分析方法:均摊时间复杂度
下面我们来看看如何使用摊还分析法来分析计算上面代码的时间复杂度;
每一次O(n)的插入操作,都会跟着n-1次O(1)的操作,所以把耗时多的那次操作均摊到接下来的n-1次耗时少的操作上,均摊下来,这一组连续的操作的均摊时间复杂度就是O(1)。
使用场景:
对一个数据结构进行一组连续操作中,大部分情况时间复杂度很低,只有个别情况下时间复杂度很高,而且这些操作存在前后连贯的时序关系,这种情况下,我们就要看是否能将较高的时间复杂度平摊到其他时间复杂度较低的操作上。在能够应用均摊时间复杂度的场合,一般均摊时间复杂度就等于最好情况的时间复杂度。
三.总结
均摊时间复杂度就是一种特殊的平均时间复杂度,掌握分析方法就行,没必要花太多精力去区别。