【数据结构与算法】【摊还分析】摊还分析详解

转载自:https://www.cnblogs.com/dongkuo/p/5824007.html







保存

摊还分析(1)

摊还分析(amortized analysis)是一种分析一个操作序列中所执行的所有操作的平均时间分析方法。与一般的平均分析方法不同的是,它不涉及概率的分析,可以保证最坏情况下每个操作的平均性能。

下面介绍瘫痪分析中的最常用的三种技术。

1. 聚合分析

1.1 栈操作

先来看对栈进行操作的例子。

通常,栈能够进行push(S, x)pop(S)操作,其时间复杂度均为O(1)。现在定义一个新的操作multipop(S, k),它删除栈S栈顶的k个元素(若不足k个,则全部弹出为止)。下面是操作multipop(S, k)的伪代码:

while  stack-empty(S)  k > 
    ()
    k = k - 

可以看出,multipop的时间代价为min(s, k),其中s是栈中元素的个数。

下面我们分析一个由n个push、pop和multipop组成的操作在一个空栈上的执行情况。

简单地,根据上面对multipop操作时间代价(min(s, k))的分析,我们很快得出,一个multipop操作的最坏时间是O(n),因为栈的大小是n,因此n个操作的序列的最坏时间代价是。虽然这个结论是正确的,但是它却不是一个确界。

从另一个角度考虑,pop操作的次数至多与push操作的次数相等,而push操作的次数至多为n次。因此一个由n个push、pop和multipop组成的操作的时间代价最多为O(n),分摊到每一个操作的时间代价就为O(n) / n = O(1)。

1.2 二进制计数器递增

我们再来看二进制计数器递增的例子。

二进制计数器是用一个数组来表示一个数,即数组每一个槽充当二进制数的一个数位,数组的第一个槽为最低位。

其递增算法的思路是:首先将最低位记为当前位。判断当前位是否为1,若是,则将该位置为0,当前位右移一位(进位操作);不断重复以上过程直至当前位的值不为1或者当前位“溢出”。最后,如果没有“溢出”,就将当前位的值置为1。

下面是python实现代码:

 increment(num):
    i = 
    while i < len(num)  num[i] == :
        num[i] = 
        i += 
     i < len(num):
        num[i] = 

假设这个数组长度位k。根据上面的代码,我们可以发现,increment操作的最坏时间代价是 O(k),这在所有位上都是1时发生。因此,我们可以粗略地做出判断:对初值位0的计数器执行n个increment操作的时间代价位O(nk)。

和前面一样,事实上,这个上界是不确切的。因为increment操作不会在连续执行n次时,每次都是最坏情况。事实上,我们可以确切地计算出其时间代价:

因此,对于一个初值为0的计数器,执行一个n个increment操作的序列的最坏时间代价是O(n),分摊到每一个操作的时间代价为O(1)。

2. 核算法

在用核算法(accounting method)进行摊还分析时,我们对不同操作赋予不同的费用,它可能多于或少于其实际代价,我们称其为摊还代价

当一个操作的摊还代价超出其实际代价时,我们将差额存入数据结构的特定对象,存入的差额称为信用。对于后续操作中摊还代价小于其实际代价的情况,信用可以用来支付差额。

2.1 栈操作

回到栈操作的例子,其中各操作的实际代价为:

操作 实际代价
push 1
multipop min(k, s)

我们为这些操作赋予如下的摊还代价:

操作 摊还代价
push 2
multipop 0

在每次进行push操作时,我们花费2个单位的代价,1个单位用来支付其本身的实际代价,另1个单位可作为信用保存;当对栈中的任何一个元素进行pop(multipop也属于pop)操作时,可用该元素在push时储存的信用来支付差额。这样就保证了在任何时刻的信用值是非负的

因此,总实际代价的上界为总摊还代价,即为O(n)。

这是因为在任何时刻都成立,其中为第i个操作的真实代价,为其摊还代价。

2.2 二进制计数器递增

同理,我们也可以用核算法来分析上述二进制计数器递增的例子。

因为increment操作的时间效率与当前数组中为1的位数成正比,因此我们将翻转的位数作为操作的代价。

对于一次置位操作(将该位值置为1的操作),我们设其摊还代价为2。在进行一次置位操作时,我们花1个单位的代价来支付其本身的实际代价,剩余1单位作为信用,用来支付将来可能的复位操作(将该位值置为0的操作)的代价。这样就保证了在任何时刻的信用值是非负的。这样就保证了在任何时刻的信用值是非负的

因此,总实际代价的上界为总摊还代价。而在一次increment操作中,只进行一次置位操作,因此总摊还代价O(n)。

3. 势能法

核算法不同,势能分析并不是将预付代价表示为数据结构中特定对象的信用,而是表示为“势能”,简称“势”。释放势能即可用来支付未来操作代价。我们将势能与整个数据结构而不是特定对象相关联。

假设我们对一个初始数据结构执行n个操作。对每一个,用表示第i个操作的实际代价,表示在数据结构上执行第i个操作得到的结果数据结构。势函数将每个数据结构映射到一个实数,此值即为关联到数据结构的势;并且定义第i个操作的摊还代价为:

每个操作的摊还代价为其实际代价与其引起的势能变化的和

于是,个操作的总代价为:

做一个移项,变形得:

如果我们能定义一个势函数,使得,则总摊还代价给出了总实际代价的一个上界。

由于我们不是总能知道要执行多少个操作,因此无法直接保证。但是如果我们将势函数的选择条件变得严格,使得对于所有的i都成立,那么就可以保证。

3.1 栈操作

我们再次回到栈操作的例子。这次我们采用势能法来分析该问题。

对于势函数的选取,我们选择将栈映射到其内部元素个数的函数,即次操作栈时,栈中元素的个数。对于初始的空栈,有;并且由于栈中元素不可能为负,因此。

因此,以上函数定义的n个操作的总摊还代价为总实际代价的一个上界。

有了以上函数的定义,我们便可以计算栈上各种操作的摊还代价。

对于push操作,其实际代价为1,其引起的势能变化也为1,因此其摊还代价为2。

对于pop操作,同理可得其摊还代价为0。

对于一次multipop操作,它会将个对象弹出,即其实际代价为;引起的势差变化为,因此其摊还代价也为0。

综上,每个操作的摊还代价都为O(1),n个操作的总摊还代价为,总实际代价最坏为。

3.2 二进制计数器递增

同样我们再用势能法来分析二进制计数器递增的例子。

与上面相似,我们将表示为二进制计数器中,位上是1的位数。显然。因此,总摊还代价为总实际代价的一个上界。

再分析总摊还代价。对于一次increment操作,其实际代价为次复位操作和1次置位操作,为;其引起的势差变化为,因此摊还代价为,个操作的总摊还代价为,因此总实际代价最坏为。



猜你喜欢

转载自blog.csdn.net/u013718730/article/details/80233844