数据结构与算法-最好、最坏、平均、均摊时间复杂度

一,前言

上一篇介绍了复杂度分析
但是,同一段代码在不同的输入情况下,复杂度量级有可能是不一样的

例如:在大小为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)

均摊时间复杂度和摊还分析法的应用场景比较特殊,大致如下:

对数据结构进行一组连续操作,大部分情况时间复杂度很低,个别情况时间复杂度较高,
连续的操作间存在明确的先后时序关系,
此时可以考虑将时间复杂度较高的操作,平摊到其他时间复杂度较低的操作上	

均摊时间复杂度就是一种特殊的平均时间复杂度
在能够使用的场景中,一般均摊时间复杂度就等于最好情况时间复杂度


四,结尾

数据结构和算法解决的是快和省的问题
复杂度分析表示的是随数据量级增加,代码执行效率和空间占用的变化趋势

当同一段代码在不同输入下,存在复杂度量级改变时,将继续评估其复杂度

四种时间复杂度:

最好情况时间复杂度
最坏情况时间复杂度
平均情况时间复杂度
均摊时间复杂度(摊还分析法)

你可能感兴趣的:(算法和数据结构)