针对上一篇中的Dynamic Array,我们知道当在数组末尾插入新的元素的时候,根据不同位置程序的运行时间也不相同。
1. 假设当前我们有一个 Capacity 为3的动态数组。在数组中依次插入a,b,c,每次对应的操作时间为O(1)。
2. 继续在数组中插入d,e,f。由于之前的capacity不够,因此首先要把容量变成原来的两倍,然后把之前的元素拷贝到大容量的数组中。
根据上面的内容我们可以知道,如果在动态数组中插入元素,其每次的运行时间是不同的。运行时间最少的情况是O(1),而运行时间最长的情况是O(n)。因此如果要知道尾部插入新的元素的运行时间究竟应该是多少,此时我们就需要用到 Amortized Analysis 来解决这个问题。
Amortized Analysis 适用情况是当一种算法既有耗时长的运算情况,又有耗时短的运行情况。
方式一:Aggregate Method
Aggregate Method的核心就是去求运行时间的平均值。
假设 = 插入第 i 个元素的cost,那么:(继续沿用上面的例子,初始capacity为3)
当 i - 1 是旧数列大小的倍数的时候: = 1 + ( i - 1) = i
否则: = 1
因此,平均运行时间为:
( + + ... + ) / n --> O(n) / n --> O(1)
方式二:Banker's Method
Banker‘s Method的核心就是在运行时间少的情况下,投入额外的时间。因此,在需要耗费多的事情的情况下,可以去利用之前这些额外的时间。
比如当前有一个dynamic array,capacity = 1,我们要把字母插入到这个array中。假设每次操作前都先入账3块钱,每次操作消耗1元钱。
1. 插入字母a
当前capacity = 1
起始金额 0 入账2元(注:初始入账为2元) + 2 插入字母a,消耗1元 - 1 结余 1 2. 插入字母b
扩充capacity --> 1 x 2 --> new capacity = 2
起始金额 1 将元素a从旧数组复制到新数组 - 1 入账3元 + 3 插入字母b,消耗1元 - 1 结余 2 3. 插入字母c
扩充capacity --> 2 x 2 --> new capacity = 4
起始金额 2 将元素a从旧数组复制到新数组 - 1 将元素b从旧数组复制到新数组 - 1 入账3元 + 3 插入字母c - 1 结余 2 4. 插入字母d
此时capacity = 4
起始金额 2 入账3元 + 3 插入字母d - 1 结余 4
5. 插入字母e
扩充capacity --> 4 x 2 --> new capacity = 8
起始金额 4 将元素a从旧数组复制到新数组 - 1 将元素b从旧数组复制到新数组 - 1 将元素c从旧数组复制到新数组 - 1 将元素d从旧数组复制到新数组 - 1 入账3元 + 3 插入字母e - 1 结余 2 。。。。
9. 插入字母i
扩充capacity --> 8 x 2 --> new capacity = 16
起始金额 8 将元素a至h从旧数组复制到新数组 - 8 入账3元 + 3 插入字母i - 1 结余 2
由上方可知,当使用这种方法的时候,每次插入新元素 = 预存2元花费1元,因此整体还是只有1次的操作,那么 Amotized time 也是 O(1)。
方式三:Pysicist's Method
Pysicist's Method 和 Banker's Method 类似,都是先进行 “预存” 以避免扩展数组时带来的额外花费。比如我们将一个球从高处滚下,那么滚下的期间,其势能将会转化为动能。同理,我们在对dynamic array进行插入操作的时候,也先蓄积势能,在需要扩展数组的时候,释放这个势能。
Pysicist's Method 的“预存”方式是提前扩展数组。
即当只有1个元素时,此时数组已经被扩展了,new capacity = 2
当有2个元素时,此时数组已经被扩展了,new capacity = 4
当有3个元素时,此时capacity = 4
当有4个元素时,此时数组已经被扩展了,new capacity = 8
...
以此类推
算法过程跳过,有兴趣可查看这个链接。
最后得出的时间仍然为 O(1)