《算法导论3rd第十七章》摊还分析

前言

为什么要引入摊还分析?有的时候求出的渐进上限并不准确,也就是说,由于我们一直是在考虑程序消耗时间的最坏情况,而程序并不是一直处于最坏情况!比如

int test(int n){
    
    
	int sum = 0;
	for(int i=0; i<n; i++){
    
    
		if(i % 500 == 0){
    
    
			for(int j=0; j<n; j++){
    
    
				sum += i;
			}
		}
		else{
    
    
			sum++;
		}
	}
}

该代码中,存在两个嵌套的循环。其中第5行的for循环只有在i能被500整除的时候才会执行。但是由于我们要考虑最坏情况,因此计算出来的复杂度其实就是O(n2)。但我们心里清楚,其实真实情况远不会消耗O(n2)的时间。而摊还分析,其实就是在分析时考虑了这些情况,因此根据摊还分析计算出的复杂度,就会更低一些,也更精确一些。

在摊还分析中,我们求数据结构的一个操作序列中所执行的所有操作的平均时间,来评价操作的代价。这样,我们就可以说明一个操作的平均代价是很低的,即使序列中某个单一操作的代价很高。

聚合分析

利用聚合分析,我们证明对所有n,一个n个操作的序列最坏情况下花费的总时间为T(n)。因此,在最坏情况下,每个操作的平均代价,或摊还代价为T(n) / n。

假设我们有一个栈,对这个栈有入栈和出栈的操作

  • push(s)每次只能压一个数据,所以规定操作的代价为1
  • pop(s)每次只能弹一个数据,所以也规定操作的代价为1
  • mutlipop(s,k),内部实现的是一个循环弹出,每执行一次的代价为k(k<n,n为栈的最大容量)

MULTIPOP所能进行的有效POP次数最多只能等于PUSH的次数,即:之前PUSH了多少次,则最多只能POP多少次。因此,若栈内有n个对象,则n个MULTIPOP的操作,所包含的有效POP次数最多也就是n。

对于任意的n值,任意一个由n个PUSH、POP和MULTIPOP组成的操作序列,最多花费O(n)的时间。

因此,n个操作的复杂度为O(n),则每个操作的平均复杂度为
T ( n ) = O ( n ) / n = O ( 1 ) T(n) = O(n)/n = O(1) T(n)=O(n)/n=O(1)

核算法

用核算法进行摊还分析时,我们对不同操作赋予不同费用,赋予某些操作的费用可能多于或少于其实际代价。我们将赋予一个操作的费用称为它的摊还代价。当一个操作的摊还代价超出其实际代价时,我们将差额存入数据结构中的特定对象,存入的差额称为信用(credit)。对于后续操作中摊还代价小于实际代价的情况,信用可以用来支付差额。

用通俗的语言讲,这个方法类似于买往返票的操作。也就是把一个操作A的代价和该操作未来可能需要的代价都算在这个操作的头上,这样的话,未来对与该操作相关的操作B就不用再计算复杂度了,因为B操作的代价已经由A来支付了。

再以栈操作为例,就是给PUSH操作买了一张往返票。其原因就是,每一个PUSH操作都会把一个对象压入栈中,因此该对象未来就可能会被一个POP操作所弹出。这一压一弹,代价为2。

PUSH 2
POP  0
MULTIPOP 0

因此,意一个由n个PUSH、POP和MULTIPOP组成的操作序列,其代价最高的时候就是所有操作都是PUSH的时候(因为POP和MULTIPOP代价为0),此时代价为n×2=2n,因此n个操作的复杂度为
T ( n ) = O ( 2 n ) / n = O ( 1 ) T(n) = O(2n)/n = O(1) T(n)=O(2n)/n=O(1)

势能法

势能法的思想与核算法类似,依旧是类似于买往返票这种提前支付的模式。不同的是,在核算法中,我们是用微观的方式对每一个操作赋予代价;而在势能法中,我们是用宏观的方式来对整个数据结构来进行代价的评估

这里的势能与物理学有点沾边,比如我们知道的物体的“重力势能”,是随着物体与地面距离增大而增大的。因此,以前文提到的栈这个数据结构为例,我们也可以规定它的势能。该势能初始为0,随着栈内对象数量的增大而增长,随对象数量的减少而减小

势能法定义了一条公式;
C i ( 摊 还 ) = C i ( 实 际 ) + f ( D i ) − f ( D i − 1 ) Ci(摊还)=Ci(实际)+f(Di)-f(Di-1) Ci()=Ci()+f(Di)f(Di1)

Ci 为每步操作的代价,f(Di)表示执行了第i个操作后的势能,那个这个公式就可以理解为 第i步操作的摊还代价等于第i步操作的实际代价加上从第i-1步操作到第i步操作的势能变化

总代价为
T = c 1 + c 2 + . . . + c n + ϕ ( D n ) − ϕ ( D 0 ) T=c1 + c2 +...+cn + ϕ(Dn)−ϕ(D0) T=c1+c2+...+cn+ϕ(Dn)ϕ(D0)

下面我们来看栈操作PUSH,POP和MULTIPOP的摊还代价

  • PUSH的实际代价为1,因此PUSH操作的摊还代价为
    c ^ i = c i + ϕ ( D i ) − ϕ ( D i − 1 ) = 1 + 1 = 2 \hat c_i = c_i + \phi(D_i) - \phi(D_{i-1}) = 1 + 1 = 2 c^i=ci+ϕ(Di)ϕ(Di1)=1+1=2
  • POP的实际代价为1,因此POP操作的摊还代价为
    c ^ i = c i + ϕ ( D i ) − ϕ ( D i − 1 ) = 1 − 1 = 0 \hat c_i = c_i + \phi(D_i) - \phi(D_{i-1}) = 1 -1 = 0 c^i=ci+ϕ(Di)ϕ(Di1)=11=0
  • MULTIPOP的实际代价为k’(即执行了k’个POP操作),因此MULTIPOP操作的摊还代价为
    c ^ i = c i + ϕ ( D i ) − ϕ ( D i − 1 ) = k − k = 0 \hat c_i = c_i + \phi(D_i) - \phi(D_{i-1}) = k -k = 0 c^i=ci+ϕ(Di)ϕ(Di1)=kk=0

我们得出同样的结果,即PUSH的代价为2,POP和MULTIPOP的代价为0,因此n个操作的摊还代价就是实际总代价的上界,因此n个实际操作的复杂度也为O(n)。

动态表

(略)

主要参考

算法导论随笔(十二):摊还分析(Amortized Analysis)之聚合分析、核算法和势能法
Aggregate analysis
摊还分析/平摊分析(Amortized Analysis):从白痴到入门

猜你喜欢

转载自blog.csdn.net/y3over/article/details/121849256