分摊分析[1]给我的印象是大概是心理安慰,让我们放心的用,而不去担心会有什么坏的性能问题。比如C++ 的vector 动态增长,每个操作的分摊时间复杂度是O(3)。
有三种操作手法:
聚类方法
最坏情形下,一系列操作总的代价上界除以操作个数,就是每个操作的分摊代价。这里每个操作的分摊代价相同。
例子 二进制计数器
INC(A)
i = 0
while i < A.length and A[i] == 1
A[i] = 0
i = i + 1
if i < A.length
A[i] = 1
初始时A[0..k] = 0.
INC(A)最坏是O(k), n次INC最坏达到O(nk)。
但是我们注意不是每次都O(k),只是全一时才这样。
我们来数比特位翻转的次数。第0位翻转了n次,
第1位翻转了n/2次,总共< 2n。每个INC的分摊代价是O(2)。
记账方法
每种操作类型分摊代价可能不同,高于实际代价的差值称为prepaid credit。
例子 PUSH,POP,MULTIPOP(k)的实际代价分别是1,1,min(k,s),
而赋予分摊代价分别为2,0,0。当PUSH时,消耗1,剩余的1存入该对象,当POP时,
使用该对象上存入的1。
势能方法
把credit当成整个数据结构的势能,而不是单个树据对象的。
例1 红黑树重建的代价
RB_Insert/RB_Delete 首先需要O(lgN)查找到key所在节点,然后只分别需要O(1)的节点插入,节点删除和旋转,但是改变颜色
最坏可能是lgN, 其他可能是多次,或零次。
定义势能函数Phi(T)=sum{w(x): x \in T}, 其中如果x是红色,或者x是黑有两个红孩子,则w(x)=0; 如果x是黑,没有红孩子,则w(x)=1; 如果x是黑,有两个红孩子,则w(x)=2。
证明:任一RB_Insert/RB_Delete后,Phi(T)至少减少一。从任意混合的插入删除序列的分摊代价是O(1)。
例2 自组织链表:
搜索必须从链表头开始,如果在第k项找到,则代价为k。
搜索后允许使用任何启发式方法调整链表顺序。
heuristic knowing entire access sequence.虽然我不知道这种最优方法的代价,
但是我们使用势能分析,即使预先不知道存取序列,move-to-front heuristic方法的代价不超过最优方法代价的两倍。
例3 伸展树[2]
如果想要和红黑树一样的分摊复杂度,而编码复杂度不要那么高,就用这个。
参考
[1] 算法导论第三版
[2] https://en.wikipedia.org/wiki/Splay_tree