最好、最坏、平均、均摊时间复杂度

关注公众号 MageByte,设置星标点「在看」是我们创造好文的动力。后台回复 “加群” 进入技术交流群获更多技术成长。 本文来自 MageByte-青叶编写

上次我们说过 时间复杂度与空间复度,列举了一些分析技巧以及一些常见的复杂度分析比如 O(1)、O(logn)、O(n)、O(nlogn),今天会继续细化时间复杂度。

1. 最好情况时间复杂度(best case time complexity)

2.最坏情况时间复杂度(worst case time complexity)

3. 平均情况时间复杂度(average case time complexity)

4.均摊时间复杂度(amortized time complexity)

复杂度分析

public int findGirl(int[] girlArray, int number) {
  int i = 0;
  int pos = -1;
  int n = girlArray.lentgh();
  for (; i < n; ++i) {
    if (girlArray[i] == number) {
      pos = i;
      break;
    }
  }
  return pos;
}

代码逻辑你应该很容易看出来,在无序数组 中查找 number 出现的位置,如果没找到就返回 -1。《唐伯虎点秋香》主角星爷通过这个方法遍历数组找到秋香,因为此刻我们还没有学会各种风骚的算法,只能从头到尾查验是不是秋香,所以只能遍历数组。girlArray 数组保存着秋香、冬香、春香……的编码,现在唐伯虎通过 选择 number 这个编码比对是否是秋香。

这段代码在不同的情况下,时间复杂度是不一样的,所以为了描述代码在不同情况下的不同时间复杂度,我们引入了==最好、最坏、平均时间复杂度==。n = girlArray 数组的长度。

  1. 当如秋香在第一个,那 代码的时间复杂度就是 O(1)。
  2. 当秋香在队伍的最后一个,那代码的时间复杂度就是 O(n)。
  3. 当 秋香在队伍中但是不在队伍第一个,也不再最后一个,那么就不确定。
  4. 假如华府使诈,队伍里也根本不存在秋香,唐波徐也需要把队伍一个个查验完毕才知道,时间复杂度就成了 O(n)

最好情况时间复杂度

在最理想的情况下,执行这段代码的时间,也就是「唐伯虎」最快点中秋香。假如 这一排姑娘就代表 girlArray 数组,number 变量就是秋香的编码。假如第一个姑娘就是「秋香」那时间复杂度就是 O(1)。

最坏情况时间复杂度

在最糟糕的情况下,执行这段代码的时间复杂度。也就是要一个个查验真个数组的长度 O(n)。

平均情况时间复杂度

其实最好与最坏情况是极端情况,发生的概率并不大。所以为了更准确的表示平均情况下的时间复杂度,引入另一个改变:平均情况时间复杂度

还是上面的「找秋香」代码,判断 number 编码在循环中出现的位置,有 ==n + 1==种情况:

在数组 0~n-1 中和不在这个数组中。在数组中共有 n 种情况,加上不在数组中则就是 n + 1 种了。 每种情况要遍历的姑娘人数都不同。我们把每种情况需要查找姑娘的数量累加,然后再除以 所有情况数量 (n + 1),就得到需要遍历次数的平均值。敲黑板了:公式就是平均情况复杂度 = 累加每种遍历的元素个数 / 所有的情况数量

平均情况复杂度为:

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

推导过程:

$$\because 1+2+3 …+ n = n + (n-1) + (n-2)… + 1$$

$$\therefore (1 +2 +3… + n) = \frac {n(1+n)} {2}$$

$$\therefore (1+2+3+…+n) + n = \frac {n(3+n)} {2}$$

根据我们之前学的 时间复杂度与空间复度 大 O 表示法,省略系数、地接、常量,所以平均情况时间复杂度是 O(n)

期望时间复杂度

上面的平均情况时间复杂度推导没有考虑每种情况的发生概率,这里的 n+1 种情况,每种情况发生的概率是不一样的,所以还要引入各自发生的概率再具体分析。

秋香的编号 number 要么在 0 ~ n-1 中,要么不在 0~n-1 中,所以他们的概率是 $\frac {1} {2}$。

同时 number 在 0~n-1 各个位置的概率是一样的为 1/n。根据概率乘法法则,number 在 0~n-1 中任意位置的概率是 $$\frac {1} {2n}$$。

所以在前面推导的基础上,我们再把每种情况发生的概率考虑进去,那么平均情况时间复杂度的计算过程就是:

考虑概率的平均情况复杂度:

$$(1 \frac {1} {2n} + 2 \frac {1} {2n}+ 3 \frac {1} {2n}…+n\frac {n} {2n} ) + n \frac {1} {2} = \frac {3n+1} {4}$$

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

引入概率之后,平均复杂度变为 O($$\frac {3n+1} {4}$$),忽略系数以及常量,最后得到的加权平均时间复杂度为 O(n)。终于分析推导完了,同学们可以松一口气。

注意:

多数情况下,我们不需要区分最好、最坏、平均情况时间复杂度。只有同一块代码在不同情况下时间复杂度有量级差距,我们才会区分 3 种情况,为的是更有效的描述代码的时间复杂度。

均摊情况时间复杂度

最后一个硬骨头来了,了解了上面加上概率的期望时间复杂度再看这个就容易多了。均摊时间复杂度,听起来跟平均时间复杂度有点儿像。

均摊复杂度是一个更加高级的概念,它是一种特殊的情况,应用的场景也更加特殊和有限。

对应的分析方式称为:摊还分析或平摊分析。

// array 表示一个长度为 n 的数组
// 代码中的 array.length 就等于 n
 int[] array = new int[n];
 int count = 0;

public 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.lenth,遍历数组求和,将求和之后的 sum 值放到数组的第一个位置,然后再将新的数据插入。但如果数组一开始就有空闲空间,则直接将数据插入数组。这里的数据满:对于可反复读写的存储空间,使用者认为它是空的它就是空的。如果你定义清空是全部重写为 0 或者某个值,那也可以!使用者只关心要存的新值!

分析上述的时间复杂度:

  1. 最理想情况,有空闲空间则直接插入到数组下标 count 的位置即可。所以是 O(1)。
  2. 最坏的情况,数组没有空闲空间,需要先做一次循环遍历求和,然后再插入。时间复杂度 O(n)。

平均时间复杂度

数组长度为 n,因为可以插入不同位置,所以有 n 种情况,每种复杂度为 O(1)。

还有一种特殊情况,没有空闲空间插入的时候,复杂度是 O(n),一共就是 n+1 种情况,且每种情况的概率都是 $$\frac{1} {n+1}$$。所以根据加权平均计算法,平均时间复杂度:

$$(1 \frac {1} {n+1} + 1 \frac {1} {n+1}+ 1 \frac {1} {n+1}…+1\frac {1} {n+1} ) + n \frac {1} {n+1} = \frac {2n} {n+1}$$

当省略系数及常量后,平均时间复杂度为 O(1)。

其实我们不需要这么复杂,对比 findGirl 跟 insert 方法。

  1. findGirl 在极端情况下复杂度 O(1),而 insert 基本情况是 O(1)。只有当数组满了才是 O(n)。
  2. 对于 insert() 函数来说,O(1) 时间复杂度的插入和 O(n) 时间复杂度的插入,出现的频率是非常有规律的,而且有一定的前后时序关系,一般都是一个 O(n) 插入之后,紧跟着 n-1 个 O(1) 的插入操作,循环往复。

摊还分析法

分析上述示例的平均复杂度分析并不需要如此复杂,无需引入概率论的知识。

因为通过分析可以看出,上述示例代码复杂度大多数为 O(1),极端情况下复杂度才较高为 O(n)。同时复杂度遵循一定的规律,一般为 1 个 O(n),和 n 个 O(1)。针对这样一种特殊场景使用更简单的分析方法:摊还分析法

通过摊还分析法得到的时间复杂度为均摊时间复杂度

大致思路:每一次 O(n)都会跟着 n 次 O(1),所以把耗时多的复杂度均摊到耗时低的复杂度。得到的均摊时间复杂度为 O(1)。

应用场景:均摊时间复杂度和摊还分析应用场景较为特殊,对一个数据进行连续操作,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度较高。而这组操作其存在前后连贯的时序关系。

这个时候我们将这一组操作放在一起分析,将高复杂度均摊到其余低复杂度上,所以一般均摊时间复杂度就等于最好情况时间复杂度。

注意: 均摊时间复杂度是一种特殊的平均复杂度(特殊应用场景下使用),掌握分析方式即可。

均摊时间复杂度就是一种特殊的平均时间复杂度,我们没必要花太多精力去区分它们。你最应该掌握的是它的分析方法,摊还分析。至于分析出来的结果是叫平均还是叫均摊,这只是个说法,并不重要。

文末思考

最后留一个问题给大家,用本文学习的只是分析下面代码的「最好」、「最坏」、「均摊」时间复杂度。

/ 全局变量,大小为 10 的数组 array,长度 len,下标 i。
int array[] = new int[10];
int len = 10;
int i = 0;

// 往数组中添加一个元素
void add(int element) {
   if (i >= len) { // 数组空间不够了
     // 重新申请一个 2 倍大小的数组空间
     int new_array[] = new int[len*2];
     // 把原来 array 数组中的数据依次 copy 到 new_array
     for (int j = 0; j < len; ++j) {
       new_array[j] = array[j];
     }
     // new_array 复制给 array,array 现在大小就是 2 倍 len 了
     array = new_array;
     len = 2 * len;
   }
   // 将 element 放到下标为 i 的位置,下标 i 加一
   array[i] = element;
   ++i;
}

总体的含义就是向数组添加一个元素,当空间不够的时候重新生情一个原来两倍空间的数组并把原来的数组数据依次复制到新数组中。

其实同学们这里还可以拓展到 HashMap 的拓容,当元素大刀负载因子 0.75 的容量,HashMap 需要拓容为原来的两倍然后再重新 把元素放到新数组中。那么时间复杂度又是多少呢?

关注公众号 MageByte 后台回复 「add」获取本题目答案,也可以回复「加群」加入技术群跟我们一起分享你的看法,我们第一是时间反馈。

参考文献:《数据结构与算法之美》

原文地址:https://blog.51cto.com/14745561/2478783

时间: 2024-07-31 22:06:52

最好、最坏、平均、均摊时间复杂度的相关文章

Chapter4 复杂度分析(下):浅析最好,最坏,平均,均摊时间复杂度

四个复杂度分析: 1:最好情况时间复杂度(best case time complexity) 2:最坏情况时间复杂度(worst case time complexity) 3:平均情况时间复杂度(average case time complexity) 4:均摊时间复杂度(amortized time complexity) for (; i < n; ++i) { if (array[i] == x) { pos = i; break; } } 分析:1:最好情况时间复杂度:O(1) 2

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

时间复杂度分析有哪些? 最好情况时间复杂度(best case time complexity) 最坏情况时间复杂度(worst case time complexity) 平均情况时间复杂度(average case time complexity) 均摊时间复杂度(amortized time complexity) 最好.最坏情况时间复杂度 最好情况时间复杂度就是在最理想的情况下,执行这段代码的时间复杂度. 最好情况时间复杂度就是在最糟糕的情况下,执行这段代码的时间复杂度. 来看看下面这段

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

由来 /**** 在一个无序的数组(array)中 查找变量 x 第一次出现的位置.如果没有找到,就返回 -1 ****/ // 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; } 分析出此函数的时间复杂度为 O(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;  }  return pos;} 时间复杂度是O(n) // n 表示数组 array 的长度int find(int[] array, int n, int x) {  int i = 0;  int pos = -1; 

【loj6029】「雅礼集训 2017 Day1」市场 线段树+均摊分析

题目描述 给出一个长度为 $n$ 的序列,支持 $m$ 次操作,操作有四种:区间加.区间下取整除.区间求最小值.区间求和. $n\le 100000$ ,每次加的数在 $[-10^4,10^4]$ 之间,每次除的数在 $[2,10^9]$ 之间. 题解 线段树+均摊分析 和 [uoj#228]基础数据结构练习题 类似的均摊分析题. 对于原来的两个数 $a$ 和 $b$ ( $a>b$ ) ,原来的差是 $a-b$ ,都除以 $d$ 后的差是 $\frac{a-b}d$ ,相当于差也除了 $d$

【BZOJ3211】花神游历各国 树状数组 并查集 均摊分析

链接: #include <stdio.h> int main() { puts("转载请注明出处[vmurder]谢谢"); puts("网址:blog.csdn.net/vmurder/article/details/44686727"); } 题解: 一个点开几次方就没啦.所以我们只需要修改不是0或者1的点就行了. 均摊基本O(n). 然后用并查集维护一个点右边第一个不是0的数. 手写读入果然高大上.卡rank神器. 顺便Orz一下wys大神. 代

cf250D. The Child and Sequence(线段树 均摊复杂度)

题意 题目链接 单点修改,区间mod,区间和 Sol 如果x > mod ,那么 x % mod < x / 2 证明: 即得易见平凡, 仿照上例显然, 留作习题答案略, 读者自证不难. 反之亦然同理, 推论自然成立, 略去过程Q.E.D., 由上可知证毕. 然后维护个最大值就做完了.. 复杂度不知道是一个log还是两个log,大概是两个吧(线段树一个+最多改log次.) #include<bits/stdc++.h> #define Pair pair<int, int&g

算法之时间复杂度和空间复杂度

时间复杂度: 定义:在进行算法分析时,语句的总执行次数T(n)是关于问题的规模n的函数,进而分析T(n)随着n的变化情况并确定T(n)的数量级.算法的时间复杂度,也就是算法的时间时间量度,记做:T(n)=O(f(n)).它表示随着问题的规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称时间复杂度.其中f(n)是问题规模n的某个函数. 这样用大写 O()来体现算法时间复杂度的记法,我们称之为大 0 记法 .一般情况下,随着 n 的增大, T(n)增长最慢的算法

[转帖]到底什么是时间复杂度

大学的时候 专业课 讲这个东西了 但是 自己一直没有仔细去学习.. 到现在也不是很清晰..感觉自己对数学里面的对数 指数 还有离散数学 学习的不好 影响自己对程序世界的理解. 需要找时间 仔细学习 再翻翻. https://www.cnblogs.com/fanyi0922/p/10779471.html 我们常常在武侠小说中看到一位内力精深的高手在学习新的招式的时候修炼速度异常惊人,我心目中最经典的片段就是倚天屠龙记中张无忌学习乾坤大挪移和太极拳的时候了,他能在极短的时间内领会常人数十年所不能