数据结构与算法之美 课程笔记三 复杂度分析(下)

一、最好、最坏情况时间复杂度(best case time complexity,worst case time complexity)

// 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;
}

在这段代码中,因为要查找的变量x可能出现在数组的任意位置,如果数组中第一个元素正好就是要查找的变量x,那时间复杂度就是O(1)。如果数组中不包含查找的变量x,那时间复杂度就成了O(n)。所以,不同的情况下,这段代码的时间复杂度是不一样的。

为了表示代码在不同情况下的不同时间复杂度,需要引入三个概念:最好情况时间复杂度、最坏情况时间复杂度和平均情况时间复杂度。

最好情况时间复杂度是在最理想的情况下,执行这段代码的时间复杂度。

最好情况时间复杂度是在最糟糕的情况下,执行这段代码的时间复杂度。

二、平均情况时间复杂度(Average case time complexity)

为了更好的表示平均情况下的复杂度,引入了平均情况时间复杂度,简称平均时间复杂度。

要查找的变量x在数组中的位置,有n+1种情况:在数组的o~n-1位置中和不在数组中。我们把每种情况下,查找需要遍历的元素个数累加起来,然后再除以n+1,就可以得到需要遍历的元素个数的平均值,即:

\frac{1+2+3+...+n+n}{n+1} = \frac{n(n+3)}{2(n+1)}

公式化简后得到平均时间复杂度为O(n)。

上面的公式在计算中存在一些问题,这n+1种情况,出现的概率并不是一样的。具体的,要查找的变量x,要么在数组中,要么不在数组中,这两种情况的概率并不好统计,我们这里假设都是1/2。要查找的变量出现在0~n-1这n个位置上的概率是一样的,都是1/n,所以,要查找的变量出现在0~n-1中任意位置的概率就是1/(2n)(注,这种情况下变量首先要在数组中,概率是1/2,再乘以位置概率1/n,故概率为1/(2n))。

1 \times \frac{1}{2n} + 2 \times \frac{1}{2n} + 2 \times \frac{1}{2n} + ... + n \times \frac{1}{2n} + n \times \frac{1}{2} = \frac{3n+1}{4}

这个值就是概率论中的加权平均值,也叫做期望值。所以平均时间复杂度的全称应为加权平均时间复杂度或者期望时间复杂度。

用大O表示法来表示,这段代码的加权平均时间复杂度仍然是O(n)。

在大多数情况下,并不需要区分最好、最坏、平均情况时间复杂度三种情况。只有同一块代码在不同的情况下,时间复杂度有量级的差距,才会使用这三种复杂度表示法来区分。

三、均摊时间复杂度(amortized time complexity)

// 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;
}

这段代码实现了向数组中插入数据的功能,当数组满了,就对数组中所有元素求和,并清空数组,将和放到数组的第一个位置,然后再插入新的数据。如果数组有空位置,则直接插入。

最理想的情况下,数组有空闲空间,直接插入,最好时间复杂度为O(1)。最坏的情况下,需要对数据先进行遍历求和,最坏时间复杂度为O(n)。

假设数组的长度为n,根据数据插入位置不同,可以分为n种情况,每种情况的时间复杂度为O(1),另外,还有一种数组满了的情况,该种情况的时间复杂度为O(n)。这n+1中情况发生的概率是一样的,都是1/(n+1)。得到的平均时间复杂度就是:

1 \times \frac{1}{n+1} + 1 \times \frac{1}{n+1} + ... + 1 \times \frac{1}{n+1} + n \times \frac{1}{n+1} = O(1)

find() 和insert()实例的不同:

首先,find()函数在极端情况下,复杂度才为O(1),但insert()在大部分情况下,复杂度都是O(1)。只有个别情况下,复杂度才较高,为O(n)。

其次,对insert()函数来说,O(1)复杂度的插入和O(n)复杂度的插入,出现的频率是非常有规律的,且有一定的前后时序关系,一般都是一个O(n)插入之后紧跟着n-1个O(1)的插入操作,循环往复。

针对这种特殊的场景,引入一种更为简单的分析方法:摊还分析法,通过摊还分析得到的时间复杂度称为均摊时间复杂度。

摊还分析法:

每一次O(n)的插入操作,都会跟着n-1次O(1)的插入操作,所以把耗时多的那一次操作均摊到接下来的n-1次耗时少的操作上,均摊下来,这一组连续操作的均摊时间复杂度为O(1)。

均摊时间复杂度和摊还分析应用场景比较特殊,所以不会经常用到。它的应用场景为:对一个数据结构进行一组连续操作中,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,而且这些操作之间存在前后连贯的时序关系,这时,我们就可以将这一组操作放在一起分析,看是否能将较高时间复杂度那次操作的耗时平摊到其他那些时间复杂度比较低的操作上。而且,在能够应用均摊时间复杂度分析的场合,一般均摊时间复杂度就等于最好情况时间复杂度。

极客时间版权所有: https://time.geekbang.org/column/article/40447

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值