算法复杂度分析(二)

先看以下的代码

算法复杂度分析(二)_第1张图片
代码1

    fun 函数的作用是找到数组中的目标值,没有则返回 -1,这个函数的时间复杂度,取绝于数组的长度(规模),所以可以 粗略 分析出这段代码的时间复杂度是 O(n),n 为数组长度。

    但是,在这个查找的过程中,并不是每次都需要找到最后,当提前到找到目标值,这个函数就会 return ,提前终止。比如,目标 target 就在数组的第一个,那么就不用再取遍历 剩下的 n-1 个数据了,那么这段代码的时间复杂度就是 O(1) ;如果目标值 target 在最后一个或者没有,那么这段代码的时间复杂度就是 O(n)。 

    所以,为了表示代码在不同情况下的不同时间复杂度,就出现了三个概念:最好情况时间复杂度、最坏情况时间复杂度平均情况时间复杂度 帮助我们更好的分析。


最好、最坏情况时间复杂度

    顾名思义,最好情况时间复杂度 和 最坏情况时间复杂度  分别是 指 在最理想的情况下 在最糟糕的情况下 执行这段代码的时间复杂度。对于上面代码,最好情况时间复杂度 和 最坏情况时间复杂度 分别 是 O(1) 和 O(n) 。


平均情况时间复杂度

     我们知道 最好情况时间复杂度 和 最坏情况时间复杂度 都是一种 极端 的情况, 出现的概率并不是很大。但是由于目标元素的位置不同,导致时间复杂度会出现 量级 的差异,所以 我们需要引入另一个概念 平均情况时间复杂度 来分析这种情况。

     目标元素 如果出现在 数组种,出现的位置有 n (n 为数组的长度) 种情况,还有一种是 不在数组种的情况,所以是 n + 1 种。

    我们来 求平均比对多少个数组元素才能找到 x。如果 x 再第一个位置,那需要 1 次比对,如果再第二个位置,就需要比对 2 次,一次类推,如果在第 n 个位置, 就需要比对 n 次。如果不在数组中,也需要比对 n 次。所有的次数之和除以 n+1 种情况,就是平均比对元素个数。

    平均遍历次数 = 各种情况遍历次数相加 ÷ 总的情况数

    平均次数 = (1 + 2 + 3 + 4 + ....... + (n-1) + n + n) / n + 1

                   = ( 3 + 3 + 4 + ....... + (n-1) + n + n) / n + 1

                   = n (n + 3) / 2(n + 1)

    根据大 O 表示法,忽略系数 和 低阶项 得到 平均时间复杂度为 O(n)。

   上面的情况 没有考虑各自情况发生的概率,这 n + 1 种情况 出现的概率并不是相同的。粗略的计算,目标 target 在数组 和 不在数组的概率各占 50%, 在加上出现 0 - n-1 这 n 个位置的概率也是一样的,为 1 / n 。 所以 目标值 出现在 0 - n-1 中 任意位置 的概率 为 1/ 2n。

    所以加入概率后的 平均时间复杂度 就变成了

      1 * 1/ 2n + 2 * 1/ 2n + ......n * 1/ 2n + n * 1/ 2n = (3n + 1) / 4

     这个值就是概率论中的加权平均值( 加权平均值 即 将各数值乘以相应的 权数,然后加总求和得到总体值,再除以总的单位数), 也叫做期望值。这种 时间复杂度称为 加权平均时间复杂度 或者 期望时间复杂度。

    根据大 O 表示法,忽略系数 和 低阶项 得到 加权平均时间复杂为 O(n)。

   再次提醒: 一般情况下,我们只要使用 一个时间复杂度就可以了。只有同一块代码不同的情况下,时间复杂度有 量级 的差距,我们才会使用以上三种复杂度表示法来区分。


均摊时间复杂度

算法复杂度分析(二)_第2张图片
代码2

      以上代码中,add 函数就是实现一个往数组中添加数据的功能。先定义一个长度为 n 的空数组,然后添加数据。当 coutn < n 时,直接将数据添加到数组中 ; 当  count === n 也就是等于数组的长度 时,用 for 循环遍历数组求和,将求和之后的 sum 值放到数组的第一个位置,然后再将新的数据插入。

    我们来分析一下它的时间复杂度,数组的长度为 n,根据数据插入的不同位置,可以分为 n 种情况,每种情况的时间复杂度为 O(1)。还有一种最“糟糕”的情况,那就是数组已满,这个时候的时间复杂度为 O(n)。而且,这 n+1 种情况发生的概率是一样的,都是 1/(n+1)。所以,根据加权平均的计算方法:

       1 x 1 / n+1 + 1 x 1 / n+1 + …… + 1 x 1  /n+1 + n x 1 / n+1 = O(1)

    我们求得的平均时间复杂度就是:O(1)。

    但是这个例子里的平均复杂度分析其实并不需要这么复杂,不需要引入概率论的知识。这是为什么呢?我们先来对比一下这个add 的例子和前面那个 fun 的例子,你就会发现这两者有很大差别。

    代码1 的 fun 函数在极端情况下,时间复杂度才为 O(1)。但 add 函数在大部分情况下,时间复杂度都为 O(1)。只有个别情况下,时间复杂度才比较高为 O(n)。

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

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

     针对这种特殊的情况,我们引入了一种更加简单的分析方法 摊还分析法。均摊时间复杂度对应的分析方法叫 摊还分析。平均复杂度只在某些特殊情况下才会用到,而均摊时间复杂度应用的场景就更加特殊和有限。

     在代码执行的所有复杂度情况中 绝大部分 低级别的复杂度个别情况高级别复杂度 且发生具有 时序关系 时,可以将个别高级别复杂度 均摊 到低级别复杂度上。基本上均摊结果就等于低级别复杂度。

    比如,每当经历 n 次时间复杂度为 O(1) 的操作时,便经历 1 次时间复杂度为 O(n) 的操作,有一定的时序规律,并且出现高级别复杂度的情况极少。我们将出现高级别的情况均摊到低级别复杂度的情况中,整个插入操作的时间复杂度就变为 O(1) 了这就是摊还分析的大致思想。

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

     简单来讲 出现O(1)的次数 远大于出现O(n)出现的次数,那么平均平摊时间复杂度就是O(1)。

总结

    1.代码在不同情况下复杂度出现量级差别,则用平均情况时间复杂度分析。

    2.代码在绝大多数情况下是低级别复杂度,只有极少数情况是高级别复杂度,并且具有一定的时序规律,则用均摊时间复杂度分析。

你可能感兴趣的:(算法复杂度分析(二))