摊还分析
本章内容:
1.聚合分析
2.核算法
3.势能法
4.动态表
一 聚合分析
1. 在摊还分析中,我们求数据结构的一个操作序列中所执行的所有操作的平均时间,来评价操作的代价,它不涉及概率,可以保证最坏情况下每个操作的平均性能。
2. 摊还代价:对所有n,一个n个操作的序列最坏情况下话费时间为T(n),从而摊还代价(平均代价)为 T(n) / n.
3. 栈操作中加入MULTIPOP(S, k),可以同时删除栈顶的k个元素,总元素少于k则全部删除。
下面分析一个由n个PUSH, POP, 和MULTIPOP组成的操作序列在一个空栈上的执行情况。假设栈的大小最大为n,那么MULTIPOP的最坏情况代价是O(n)。因此任意一个操作的最坏情况是O(n),从而n个操作的序列的最坏情况代价为O(n ^ 2)。
上面并不是一个确界,用聚合分析考虑整个序列的n个操作可以得到更好的上界。在一个空栈上执行n个PUSH, POP, MULTIPOP操作,考虑到每个对象PUSH进入后最多只能POP一次,因此执行POP的次数不会多于PUSH的次数,因此最多有n次,即操作序列最多花费O(n)时间,平均代价即为O(n) / n = O(1).
4. k位二进制计数器递增:
用一个length为k的位数组A[0..K - 1]作为计数器。当保存值为x时,最低位在A[0],最高位在A[k - 1],有
初始x为0,为了实现计数器加1,使用如下过程:
粗略分析发现执行一次最坏情况需要将k个位置由1变为0,所以n此操作最坏情况似乎是O(nk)
进一步分析,每次INCREMENT时A[0]确实都要翻转,但是A[1]只需要2次执行翻转一次,A[2]则4次执行翻转一次...因此n个操作的序列,A[i]会翻转n / 2 ^ i 次,因此执行序列的翻转操作总数:
显然摊还代价为O(n) / n = O(1).
二 核算法
1. 对不同的操作赋予不同的费用,将赋予一个操作的费用称为它的摊还代价。当一个操作的摊还代价超出实际代价时,将差额存入数据结构中,称为信用,可以用来支付别的操作摊还代价比实际代价少的差额。
2.
左边表示摊还代价总和,右边表示真实代价总和。总的信用等于上面左边减去右边,由于要保证摊还代价为实际代价上界,所以当前的总信用要保持为非负值。
3. 同聚合分析中的栈操作:
操作的实际代价为: 其中s为调用时栈的元素个数
我们给这些操作赋予如下的摊还代价:
可以这样进行考虑,每个元素PUSH进去的时候,需要支付2个单位的代价,其中PUSH操作实际用了一个,剩下的一个代价存起来。这样栈中的每个存在的元素其实自身都含有一个单位可用的代价,在出栈时无论是POP还是MULTIPOP都可以支付自己的出栈代价。由于栈里面的元素个数始终非负,所以可以保证信用始终非负。
因此,对任意n个操作序列的总摊还代价为实际代价的上界,即O(n).
4. 二进制计算器递增:
执行INCREMENT操作的运行时间与翻转的位数成正比,所以用翻转的位数作为操作的代价。
对一次置位操作(将该位由0变为1),设其摊还代价为2。进行置位时,用1来支付置位操作的实际代价,剩下的1存起来作为信用,该以后复位(变为0)的时候用。任何时刻信用值非负,因为计算器中1的个数始终非负。由于每次操作最多只进行一次置位(第6行),所以总摊还代价为O(n),为实际代价的上界。
三 势能法
1. 对一个初始数据结构D0执行n个操作。Di为在数据结构Di-1上执行第 i 个操作得到的结果数据结构。势函数将Di映射到一个实数Φ(Di),表示数据结构Di的势能,类似于前面的信用。有:
可以理解为摊还代价减去实际代价等于势能的增量。
n个操作总摊还代价:
将初始势能简单定义为0,然后要保证对所有 i 有Di的势能大于等于0.
2. 栈操作:
这里将势函数定义为一个栈中元素的数量,同时保证了永远非负。
第 i 个操作如果是PUSH,此时栈中有s个元素,则:
由此得到PUSH的摊还代价:
第 i 个操作如果是MULTIPOP,势能的减少恰好等于元素个数的减少,也等于实际操作的代价,有:
由此每个操作摊还代价都是O(1),所以也可以得到总摊还代价O(n)
四 动态表
1. 我们经常无法预先知道将会有多少个对象存储在表中,所以使用动态的表可以节省很多空间。现在就研究这种动态扩张和收缩表的问题,虽然插入和删除操作可能会引起扩张和收缩,但是可以用摊还分析证明它们的摊还代价都是O(1),而且可以保证动态表中的空闲元素相对于总空间的比例永远不超过一个常数分数。
2. 定义一个非空表T的装载因子α(T)定义为表中存储的数据项的数量除以表的规模。对于空表,定义规模为0,装载因子为1.
3. 表扩张:当表的装载因子为1时,尝试插入一个新的元素将没有可用的槽,这时可以分配一个包含更多槽的新表,将原表中的元素复制过去并插入新的元素。一种常用的策略是:为新表分配2倍与旧表的槽,如果只允许插入操作,那么装载因子总是在 1/2 以上,因此浪费的空间少于总的一半。
下面是伪代码,T.num是表中的数据项数量,T.size是表的规模
分析对一个空表执行n个插入操作,其中第 i 次操作的代价为ci 。如果不用扩张,那么一次插入值需要O(1)时间。如果i - 1恰好是2的幂,则第 i 个操作会引起扩张。所以
n个操作总代价:
所以单个操作摊还代价至多为3.
4. 表扩张与收缩:与扩张类似,很容易想到可以在表的装载因子刚好小于 1/2 的时候进行收缩。但是这种方法不好,原因在于:假设现在表的装载因子恰好就在 1/2 附近,那么多次的插入和删除可能造成表的多次扩张和收缩,从而使每个操作的摊还代价变为O(n)
可以做改进:当向一个满表插入一个新数据时,仍然进行相同的扩张,但只有当装载因子小于 1/4 时才进行收缩。
定义势函数:
观察发现空表和半满的表的势为0,且势永远不可能为负,因此得出的摊还代价是实际代价的上界。
a. 第 i 个操作是插入。若操作前的装载因子大于等于 1/2 ,则与前面分析相同,摊还代价至多为3;
若操作前的装载因子小于 1/2 ,且第 i 个操作完成后装载因子仍小于 1/2 ,则第 i 个操作的摊还代价为:
若操作前的装载因子小于 1/2 ,而第 i 个操作完成后装载因子大于等于 1/2 ,则
因此插入操作摊还代价至多为3
b. 第 i 个操作为删除。若操作前的装载因子小于 1/2 ,且未引起收缩,则
若操作前的装载因子小于 1/2 ,且引起收缩,则
当操作前的装载因子大于等于 1/2,可以证明上界也是一个常数。
由此可得在动态表上执行任意n个操作的实际运行时间为O(n)
总结:摊还分析其实就是将n个操作一起进行分析,对于某些最坏情况,可以用其他一些情况少于实际代价的时间进行摊还,从而算得平均每个操作比较小的时间上界。