最好、最坏时间复杂度
我们先看一个例子:
/*
例1:查找x在数组中出现的位置,如果没有找到,返回-1。n表示数组array的长度
*/
int findIndex(int[] array, int n, int x) {
int i = 0;
int index = -1;
for (; i < n; ++i) {
if (array[i] == x) {
index = i;
//break; //暂时注释掉此行
}
}
return index;
}
当把break语句注释掉的时候,总是需要遍历整个数组,所以时间复杂度就是数组的长度,为O(n)。
当有break语句的时候,如果找到x,则会提前退出(显然这种写法更高效)。
我们知道x可能出现在数组的任何位置,可能是第一个(时间复杂度为O(1)),可能是最后一个(时间复杂度为O(n)),也可能不存在数组中(时间复杂度为O(n))。
为了表示代码在不同情况下的不同时间复杂度,我们需要引入三个概念:
最好情况时间复杂度、最坏情况时间复杂度和平均情况时间复杂度。
最好情况时间复杂度 就是,在最理想的情况下,执行这段代码的时间复杂度。
最坏情况时间复杂度 就是,在最糟糕的情况下,执行这段代码的时间复杂度
最好、最坏都是在对应的都是极端情况下的代码复杂度,发生的概率其实并不大。
平均情况时间复杂度
平均时间复杂是为了更好地表示平均情况下的复杂度。
还是上面例1,结合概率知识,我们知道,要查找的变量x在数组中的位置,有 n+1 种情况:在数组的0~n-1位置中和不在数组中。
我们暂且认为每种情况发生的概率都一样为:1/(n+1)。
每种情况的时间复杂度为:1/(n+1)1、1/(n+1)2、1/(n+1)3...1/(n+1)n、1/(n+1)*n
所以平均时间复杂度为:(所有情况下的时间复杂度的总和)/(总情况数),即:
大O表示法中,可以省略掉系数、低阶、常量,所以,得到的平均时间复杂度就是 O(n)。
其实上面的概率并不都是:1/(n+1)。
要查找的变量 x,要么在数组里,要么就不在数组里。
我们假设在数组中与不在数组中的概率都为 1/2。
查找的数据出现在 0~n-1,这 n 个位置的概率也是一样的,为 1/n,则每种情况出现的概率为1/(2n)。
每种情况的时间复杂度为:1/(2n)1、1/(2n)2、1/(2n)3...1/(2n)n
查找的数据不再数组里,则概率为1/2。时间复杂度为:1/2*n。
所以平均时间复杂度为:(所有情况下的时间复杂度的总和)/(总情况数),即:
去掉系数和常量,这段代码的加权平均时间复杂度仍为 O(n)。
这个值就是概率论中的 加权平均值,也叫作 期望值,所以平均时间复杂度的全称应该叫 加权平均时间复杂度或者 期望时间复杂度。
均摊时间复杂度
均摊时间复杂度,听起来跟平均时间复杂度有点儿像。
对于初学者来说,这两个概念确实非常容易弄混。
/*
例2:n表示数组长度,count表示数组存储数据的个数
往数组中添加数据,如果数组满了,则依次打印出来,然后清空数组
*/
int[] array = new int[n];
int count = 0;
void insert(int val) {
if (count == array.length) {
for (int i = 0; i < array.length; ++i) {
System.out.println(array[i]);
}
System.out.println(val);
array = new int[n];
count = 0;
} else {
array[count] = val;
count++;
}
}
分析下前面说的三种时间复杂度。
最好:最理想的情况下数组空闲,时间复杂度为O(1)
最差:数组恰好不空闲,时间复杂度是 O(n)
平均:假设数组的长度是 n,根据数据插入的位置的不同,我们可以分为 n 种情况,
每种情况的时间复杂度是 O(1)。除此之外,还有一种“额外”的情况,就是在数组没有空间时插入一个数据,这个时候的时间复杂度是 O(n)。而且,这 n+1 种情况发生的概率一样,都是 1/(n+1)。所以,根据加权平均的计算方法,我们求得的平均时间复杂度就是:
其实平均复杂度分析其实并不需要这么复杂,不需要引入概率论的知识。
相对例1的findIndex()函数:findIndex()函数在极端情况下,复杂度才为 O(1)。
但 insert() 在大部分情况下,时间复杂度都为 O(1)。只有个别情况下复杂度才为 O(n)
不知道你有没有注意到,对于 insert() 函数来说,O(1) 时间复杂度的插入和 O(n) 时间复杂度的插入,
出现的频率是非常有规律的,而且有一定的前后时序关系,一般都是一个 O(n) 插入之后,紧跟着n个 O(1) 的插入操作,循环往复。
针对这样一种特殊场景的复杂度分析,我们并不需要像之前讲平均复杂度分析方法那样,找出所有的输入情况及相应的发生概率,然后再计算加权平均值。
而是用一种更加简单的分析方法:摊还分析法,通过摊还分析得到的时间复杂度我们起了一个名字,叫均摊时间复杂度。
那究竟如何使用摊还分析法来分析算法的均摊时间复杂度呢?
每一次 O(n) 的插入操作,都会跟着 n次 O(1) 的插入操作,
所以把耗时多的那次操作均摊到接下来的 n 次耗时少的操作上,均摊下来,
这一组连续的操作的均摊时间复杂度就是 O(1)。这就是均摊分析的大致思路。
对一个数据结构进行一组连续操作中,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,
而且这些操作之间存在前后连贯的时序关系,这个时候,我们就可以将这一组操作放在一块儿分析,
看是否能将较高时间复杂度那次操作的耗时,平摊到其他那些时间复杂度比较低的操作上。
而且,在能够应用均摊时间复杂度分析的场合,一般均摊时间复杂度就等于最好情况时间复杂度。
小结
- 同一段代码,在不同输入的情况下,复杂度量级有可能是不一样的。所以有了最好、最坏、平均、均摊时间复杂度。
- 其中最好、最坏情况下的时间复杂度分析起来比较简单。平均、均摊两个复杂度分析相对比较复杂。
- 在大多数情况下,我们并不需要区分最好、最坏、平均情况时间复杂度三种情况。很多时候,我们使用一个复杂度就可以满足需求了。只有同一块代码在不同的情况下,时间复杂度有量级的差距,我们才会使用这三种复杂度表示法来区分。
- 平均复杂度只在某些特殊情况下才会用到,而均摊时间复杂度应用的场景比它更加特殊、更加有限。