摊还分析用来评价某个数据结构的一系列操作的平均代价,有时可能某个操作的代价特别高,但总体上来看也并非那么糟糕,可以形象的理解为把高代价的操作“分摊”到其他操作上去了,要求的就是均摊后的平均代价。
摊还分析有三种常用的技术:聚合分析,核算法,势能法。
1.聚合分析
利用聚合分析,我们可以证明对于任意的
下面讲两个简单的例子来说明。
- 栈操作
考虑一个空栈
PUSH(S,x):将对象x压入栈S中。
POP(S):将栈S的栈顶对象弹出,并返回该对象。对空栈调用POP会产生一个错误。
MULTIPOP(S,k):循环调用POP(S),弹出栈顶的k个元素(k<n,n为栈的最大容量)。
其中,第三种操作的伪代码如下:
MULTIPOP
那么,现在需要分析:执行n次栈操作最坏情况下的时间复杂度是多少?
分析:
单独看三个操作,前两个都是
我们需要对这三个操作整体分析。显然,对于一个非空的栈,可以执行的
因此,
在聚合分析中,我们将每个操作的摊还代价设定为平均代价,故三种操作的摊还代价都是
- 二进制计数器递增
考虑一个
只有一个操作
INCREMENT(A)
i=0
while i<k and A[i]==1
A[i]=0
i=i+1
if i<k
A[i]=1
那么,现在需要分析:执行n次该操作最坏情况下的时间复杂度是多少?
分析:
和上一个例子类似,单独考虑每一个操作,如果原来k个1,操作一次会把k个位置全更改一遍,代价是
考虑连续的n次操作,我们可以得知,每次调用
因此执行n次操作的总代价
故n次操作代价是
2.核算法
用核算法进行摊还分析时,我们对不同操作赋予不同费用,赋予某些操作的费用可能多于或少于其实际代价。我们将赋予一个操作的费用称为它的摊还代价。当一个操作的摊还代价超出其实际代价时,我们将正差额存入数据结构中的特定对象,存入的正差额称为信用。对于后续操作中摊还分析小于实际代价的情况,信用可以用来支付负差额。因此,我们可以将一个操作的摊还代价分解为其实际代价和信用(存入的或用掉的)。不同的操作可能有不同的摊还代价。这种方法不同于聚合分析中所有操作都赋予相同摊还代价的方式。
如果我们希望通过分析摊还代价来证明每个操作的平均代价的最坏情况很小,就应确保操作序列的总摊还代价给出了序列真实代价的上界。而且与聚合分析一样,这种关系必须对所有操作序列都成立。将第 i 个操作的摊还代价表示为
还是回到上面的两个例子。
- 栈操作
先考虑每个操作的实际代价(s是栈中元素个数):
然后我们赋予三个操作摊还代价:
假定使用¥1来代表1个单位的代价,那么每次PUSH时,除了支付¥1外,我们额外支付¥1作为可能发生的将来POP的预付费(信用),在POP时直接从信用里取,那么,任意时刻,每个栈里的元素都存在对应的预付费(信用)¥1,一定满足当前信用为非负值,即当前摊还代价是实际代价的一个上界。
因此,每次操作的摊还代价是
- 二进制计数器递增
我们设定置位(0变1)的摊还代价为2,复位(1变0)的摊还代价为0。
与上面的例子的分析方法相同,每次操作至多有1个位发生置位,这时除了支付¥1的实际代价之外,再预付将来可能发生的复位的代价¥1,那么将来复位时就可以直接从信用中拿。
因此,每次操作的摊还代价是
3.势能分析
势能法摊还分析并不将预付代价表示为数据结构中特定对象的信用,而是表示成“势能”,将势能释放即可用来支付未来操作的代价。我们将势能与整个数据结构而不是特定的对象相关联。
势能法工作方式如下。我们将对一个初始数据结构
累加即可得n步操作的总摊还代价:
如果能定义一个势函数
因此问题的关键在于定义一个合适的势函数
- 栈操作
我们定义势函数为当前栈中的元素个数,那么
下面分析每个操作的摊还代价。
假设第i个操作是PUSH,且当时栈中包含s个对象,那么
假设第i个操作是MULTIPOP(S,k),将
类似地,普通的POP操作的弹簧代价也为0。
因此,每次操作的摊还代价是
- 二进制计数器递增
我们定义第i次操作后,势函数为当前计数器中1的个数
假设第i次操作把
因此,摊还代价为
因此,每次操作的摊还代价是
另外,即使计数器不是从0开始也可以使用势能法分析。
初始时有
因此,不管计数器初值是什么,n次操作的总实际代价都是