算法基础:复杂度分析(下)浅析最好、最坏、平均、均摊时间复杂度

算法基础:复杂度分析(上)如何分析、统计算法的执行效率和资源消耗

在时间复杂度分析中,还有四个概念也是需要了解的。它们分别是:最好情况时间复杂度(best case time complexity)、最坏情况时间复杂度(worst cast time complexity)、平均情况时间复杂度(average cast time complexity)和 均摊时间复杂度(amortized 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;
        }
    }
    return pos;
}

这段代码的功能是,在一个无序的数组中,查找变量 x 出现的位置。如果没有找到就返回 -1。笼统的分析一下:核心代码执行了 n 次,所以其时间复杂度是 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)了。因为,遍历可能在数组下标为 0 ~ n-1 中的任何一个下标所指向的元素中结束循环。继续往下分析:

1. 假设,数组中第一个元素正好是要查找的变量 x ,显而易见那时间复杂度就是 O(1)。

2. 假设,数组中不存在变量 x ,那我们就需要把整个数组遍历一遍,时间复杂度就成了 O(n)。

所以,在不同的情况下,这段代码的时间复杂度是不同的。

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

这几个概念从字面意思也能理解,最好时间复杂度就是,在最理想的情况下,执行这段代码的时间复杂度。对应假设1。时间复杂度就是 O(1)。

同理,最坏时间复杂度就是,在最糟糕的情况下,执行这段代码的时间复杂度。对应假设2。时间复杂度就是O(n)。

平均时间复杂度

从定义中我们也能想明白,最好情况和最坏情况下的时间复杂度描述的都是极端情况下的代码执行过程,其实发生的概率并不大。为了更好地表示平均情况下的复杂度,我们需要引入另外一个概念:平均情况时间复杂度。

平均时间复杂度分析方法

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

这个公式的计算比较简单,所以就不做解释了。在时间复杂度的大 O 标记法中,可以省略掉系数、低阶、常量,所以,这个公式简化之后,得到的平均时间复杂度就是 O(n)。

我们知道,要查找的变量 x ,要么在数组里,要么就不在数组里。这两种情况对应的概率统计起来还是有些麻烦的,为了方便理解,我们假设在数组中与不在数组中的概率都为 1/2。另外,要查找的数据出现在 0 ~ n-1 这 n 个位置的概率也是一样的,为 1/n。所以,根据概率乘法法则,要查找的数据出现在 0 ~ n-1 中任意位置的概率就是 1/(2n)。

所以,前面的推导过程中存在的最大的问题就是,没有将各种情况发生的概率考虑进去。如果把每种情况发生的概率也考虑进去,那平均时间复杂度的计算过程就变成了这样:

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

所以,前面那段代码的加权平均值为(3n + 1)/4。用大 O 表示法来表示,去掉系数和常量,这段代码的加权时间复杂度仍然是 O(n)。

实际上,大多数情况下,我们并不需要区分最好、最坏、平均情况时间复杂度三种情况。很多时候,我们使用一个复杂度就可以满足需求了。只有同一代码在不同的情况下,时间复杂度有量级的差距,我们才会使用这三种复杂度表示法来区分。

均摊时间复杂度

均摊时间复杂度,听起来和平均时间复杂度有点相似。但是均摊时间复杂度的应用场景比平均时间复杂度更加特殊、更加有限。下面看一段代码:

// array 表示一个长度为 n 的数组
// 代码中的 array.length 就等于 n
static int[] array = new int[]{1, 2, 3, 4, 5};
static int count = 2;    
public static 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;
        System.out.println("array.length:::" + array.length + "sum:" + sum);
    }
    // 数组有空闲空间的情况
    array[count] = val;
    count++;
    System.out.println("count!=array.length:" + array.length + ",,,count::" + count);
    for (int i = 0; i < array.length; i++) {
        System.out.println("array[" + i + "] = " + array[i]);
    }
}

代码实现了一个往数组中插入数据的功能。当数组满了之后,也就是代码中的 count == array.length 时,我们用 for 循环遍历数组求和,并清空数组,将求和之后的 sum 值放到数组的第一个位置,然后再将新的数据插入。但如果数组一开始就有空闲空间,则直接将数组插入数组。

现在分析一下这段代码的时间复杂度,最理想的情况下,数组中有空闲空间,我们只需要将数据插入到下标为 count 的位置就可以了,所以最好情况时间复杂度为 O(1)。最坏的情况下,数组中没有空闲空间了,我们需要先做一次数组的遍历求和,然后再将数据插入,所以最坏情况时间复杂度为 O(n)。

接着我们按上面的方法分析一下平均时间复杂度。假设数组的长度是 n ,根据插入数据的位置不同,我们可以分为 n 种情况,每种情况的时间复杂度是 O(1)。此外,还有一种 :“额外” 的情况,就是在数组没有空闲空间时插入一个数据,这个时候的时间复杂度是 O(n)。而且,这 n + 1 中情况发生的概率是一样的,都是 1/(n + 1)。所以,根据加权平均的计算方法,我们求得的平均时间复杂度就是:

 其实,这个例子里的平均复杂度分析并不需要这么复杂,不需要使用概率的知识。我们先来对比一下这个 insert() 的例子和上面的 find() 的例子,二者的区别如下:

区别一:

首先,find() 函数在极端情况下,复杂度才为 O(1)。但 insert() 在大部分情况下,时间复杂度都为 O(1)。只有个别情况下,复杂度才比较高哦,为 O(n)。这是两者间的第一个区别。

区别二:

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

所以,针对这样一种特殊场景的复杂度分析,我们不需要像之前平均复杂度分析方法那样,找出所有的输入情况及相应的发生概率,然后再计算加权平均值。

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

那究竟如何使用摊还分析法来分析算法的均摊时间复杂度呢?

继续看这个 insert() 的例子。每一次 O(n) 的插入操作,都会跟着 n - 1次 O(1) 的插入操作,所以把耗时多的那次操作均摊到接下来的 n - 1 次耗时少的操作上,均摊下来,这一组连续的操作的均摊时间复杂度就是 O(1)。这就是均摊分析的大致思路。

使用场景:

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

均摊时间复杂度可能不是很好理解,尤其是与平均时间复杂度的区别。但是,我们可以将其理解为一种特殊的平均时间复杂度。最主要的还是应该掌握它的分析方法,摊还分析法。

之所以引入这几个复杂度的概念,是因为,同一段代码,在不同输入的情况下,复杂度量级有可能是不一样的。在引入这几个概念之后,我们可以更加全面的表示一段代码的执行效率。

本文来自极客时间 - 数据结构与算法之美专栏。特此感谢。

版权所有:https://time.geekbang.org/column/article/40447

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值