摊还分析

前端之家收集整理的这篇文章主要介绍了摊还分析前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

转载请声名出处:http://www.jb51.cc/article/p-ojgxukqi-rz.html

我们接着上一篇递归算法的复杂性分析,下面在介绍一种摊还分析的方法

一、摊还分析的概念

在摊还分析中,我们求数据结构的一个操作序列中所执行的所有操作的平均时间,来评价操作的代价。注意,摊还分析不同于上一篇博客中的平均情况分析,它并不涉及不同输入出现的概率,可以保证最坏情况下每个操作的平均性能。这样,就可以说明一个操作的平均代价是很低的,即使序列中某个单一操作的代价很高。

但首先要强调一点就是,摊还分析中赋予对象的信用或者费用,仅仅是用来分析而已,不需要也不应该出现在程序中。

我们以栈操作作为示例讲解摊还分析三种方法,然后以数据结构中动态表插入n个元素来示范如何应用摊还分析的方法

二、摊还分析的三种方法

示例一:栈操作

push(s,x):将对象x压入栈S中。

pop(s):将栈S的栈顶对象弹出,并返回该对象。对空栈调用pop会产生一个错误

由于两个操作都是O(1)时间的,我们假定其操作代价均为1。因此一个n个push和pop操作的序列的总代价为n,而n个操作的实际运行时间为O(n)。

multipop(s,k):内部实现的是一个循环弹出,弹出栈顶的k个元素(k<n,n为栈的最大容量)。此操作每执行一次的代价为k。

那么,现在需要分析:执行n次栈操作最坏情况下的时间复杂度是多少?

我相信,看过上一篇博客的读者都会有这样的第一反应,multipop(s,k)的代价最高,最高位是k=n,那么执行n次的最坏情况当然就是n·O(n)=O(n^2)啦,然而,实际上并非如此!

方法一 聚合分析

如果对所有的n,一个n个操作的序列最坏情况下花费的总时间为T(n),那么在最坏情况下,每个操作的平均代价,或摊还代价为T(n)/n。此外,此摊还代价是适用于每个操作的,即使序列中有多种类型的操作也是如此。

聚合分析要求我们要总体看问题,首先multipop(s,k)也是一个弹栈的操作,当栈里有数据的时候执行此操作才有效,所以上述提到的O(n^2)是不科学的。并且push(s,x)、pop(s)的代价是1,可想而知最坏的情况当然是前n-1次操作都是压栈,而最后一次才执行multipop(s,n-1),这样的代价也只有2(n-1)=2n-2,时间复杂度是O(n),平均下来每个操作的摊还代价就是O(1)了。

方法二 核算法

核算法比较好理解,进行摊还分析时,我们对不同操作赋予不同费用,负于某些操作的费用可能多于或少于其实际代价。我们将赋予一个操作的费用称为它的摊还代价。当一个操作的摊还代价超出其实际代价时,我们将差额存入数据结构中的特定对象,存入的差额称为信用。对于后续操作中摊还分析小于实际代价的情况,信用可以用来支付差额。因此,我们可以将一个操作的摊还代价分解为其实际代价和信用(存入的或用掉的)。不同的操作可能有不同的摊还代价。这种方法不同于聚合分析中所有操作都赋予相同摊还代价的方式。

如果我们希望通过分析摊还代价来证明每个操作的平均代价的最坏情况很小,就应确保操作序列的总摊还代价给出了序列总真实代价的上界。而且与聚合分析一样,这种关系必须对所有操作序列都成立。如果第i个操作的摊还代价表示为,真实代价为,则要求,数据结构中存储的信用恰好等于总摊还代价与总实际代价的差值,要求这一差值一直保持非负。比如在某个步骤,允许信用为负值(前面操作缴费不足,承诺在随后补齐账户欠费),那么当时的总摊还代价就小于总实际代价,对于到那个时刻为止的操作序列,总摊还代价就不再是总实际代价的上界了。因此,必须保证数据结构中的总信用永远非负。

同样是压栈的例子,我们可以赋给push(s,x)操作的摊还代价是2,相当于自己使用了1,而压进去的对象必定会弹出,剩下的1就是作为弹出时的费用,这样pop(s)、multipop(s,k)的摊还代价就是0(故我们不用再像聚合分析那样考虑这两个操作了)。根据我们上述待求的问题,最坏的情况就是所有的n步操作都是push(s,x),这样总代价就是2n,时间复杂度为O(n)。

方法三 势能法

势能法其实与核算法相似。势能法摊还分析并不将预付代价表示为数据结构中特定对象的信用,而是表示成“势能”,将势能释放即可用来支付未来操作的代价。我们将势能与整个数据结构而不是特定的对象相关联。势能法工作方式如下:

我们将一个初始数据结构D0执行n个操作。对每个i=1,2,......,n,令ci为第i个操作的实际代价,令Di为在数据结构Di-1上执行第i个操作得到的结果数据结构。势能函数f将每个数据结构Di映射到一个实数f(Di),此值即为关联到数据结构Di的势能,第i个操作的摊还代价用势能函数f定义为:


因此,第i步操作的摊还代价等于第i步操作的实际代价加上从第i-1步操作到第i步操作的势能变化。累加即可得n步操作的总摊还代价:


对势能如何理解呢?如果,则操作i在数据结构中存入能量以便以后使用;如果,则数据结构为操作i提供能量以执行。

再来看压栈的例子。我们还是假定push(s,x)、pop(s)的代价为1,multipop(s,k)的代价为k,且规定压栈一个对象势能+1,弹栈一个对象势能-1。那么很明显,f(Di)非负,f(D0)等于0。由上面的公式易知,总摊还代价就是总实际代价的上界。由摊还代价的定义可知,push(s,x)的摊还代价为2,pop(s)的摊还代价为0,multipop(s,k)的摊还代价也为0(因为弹栈操作势能要减少,刚好和操作代价相抵消,也可以理解为,势能用来支付了操作代价,所以总势能下降了),这样,大家有没有发现,就回到了核算法方法的讨论范畴,所以时间复杂度依然为O(n)。

三、动态表插入操作代价的摊还分析

这个动态表的数据结构是老师课堂上讲的例子,当时听糊涂了。后来我才理解,这张表是这样子的情况:这个表只允许插入,一旦表满了,就只会按照2的幂次进行扩充,新建一个是原来2倍的表格,然后,先将原来表中的数据插入,接着插入新的元素。现在需要求出这一过程的摊还代价。

好了,问题描述好了,接下来我们看一下如何利用上面所说的三种方法来进行分析。

(1)聚合分析

定义第i个插入操作的代价ci为:


如上表格所示,我们将插入一个元素的代价看成1,假设一开始表的大小为1,没有数据。如果表格没有被插满,此时每一次插入元素的代价为1;如果表格满了,易知此时表格的大小都为2的幂次,这是插入一个元素的代价就是原有的元素插入新表中的代价k(假设原表满时有k个元素)加上新插入的这1个元素,所以上表可以用下面的形式表示:


则总代价为:


所以,摊还代价就是:


(2)核算法

核算法用于动态表时,它赋予每个插入操作3元,即,其中1元用于支付当前的插入操作,剩下的2元存入用于后续的表格翻倍处理。当表格翻倍时,1元用于移动最新项,也就是刚才插入操作的元素把这1元保留做自己的信用;另1元用于移动旧项,也就是刚才插入操作的元素把这1元捐赠给了原本就在表中但没有信用的旧元素。


这样一来,每当表格翻倍时,原来表格中的数据所持有的信用刚好可以满足自己的插入需求,m大小的表格中,m/2的元素没有信用,而且已经完成插入操作,仿佛又回到了最初的形态,有点像递归。如此一来,每个插入操作被赋予3元的操作代价就可以满足无限次的扩展表格的需求。如下图所示:


核算法分析这个问题时,关键点在于存入的信用永远都是非负的,因而摊还代价的总和提供了实际代价的一个上界。


(3)势能法

我们定义势能函数为:


那么第i个插入操作的摊还代价为:


对于上式情况一:


对于上式情况二:

总结:在一个动态表上执行任意n个操作的实际运行时间为O(n),其中每个操作的摊还代价的上界是一个常数。


到此为止,算法的复杂性分析部分就说完了。接下来就要开始学习具体的算法了,有一些复杂性分析也会穿插其中,希望自己可以坚持住,共勉~


转载请声明出处:http://www.jb51.cc/article/p-ojgxukqi-rz.html

猜你在找的设计模式相关文章