数据结构时间复杂度_数据结构时间复杂度的摊还分析(均摊法)之一:基础

a73829316ed4bd8c2db4862a4016964a.png

摊还分析用来评价某个数据结构的一系列操作的平均代价,有时可能某个操作的代价特别高,但总体上来看也并非那么糟糕,可以形象的理解为把高代价的操作“分摊”到其他操作上去了,要求的就是均摊后的平均代价。

摊还分析有三种常用的技术:聚合分析,核算法,势能法。

1.聚合分析

利用聚合分析,我们可以证明对于任意的

,一个包含
个操作的序列花费的总时间为
。因此,在最坏情况下,每个操作的平均代价,或称为摊还代价为
,即每个操作的时间复杂度为

下面讲两个简单的例子来说明。

  • 栈操作

考虑一个空栈

,有三种操作:
PUSH(S,x):将对象x压入栈S中。  
POP(S):将栈S的栈顶对象弹出,并返回该对象。对空栈调用POP会产生一个错误。 
MULTIPOP(S,k):循环调用POP(S),弹出栈顶的k个元素(k<n,n为栈的最大容量)。

其中,第三种操作的伪代码如下:

MULTIPOP

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

分析:

单独看三个操作,前两个都是

的,第三个是
的。这样直观的看,最坏情况下,执行
次操作的代价是
,但这实际上是一个松的上界。因为要想达到这个上界,就要尽量多执行第三个操作,但是栈为空以后,第三个操作就什么都不干了。

我们需要对这三个操作整体分析。显然,对于一个非空的栈,可以执行的

操作的个数(包括后两个操作的所有POP)与执行了
操作的个数相当,即最多

因此,

次操作最坏情况下执行的时间复杂度为
,平均每次操作的代价是

在聚合分析中,我们将每个操作的摊还代价设定为平均代价,故三种操作的摊还代价都是

  • 二进制计数器递增

考虑一个

位二进制计数器,其初值为0,用一个数组
来表示,当计数器中保存的值是
时,
的最低位保存在
最高为保存在
,即

只有一个操作

,即计数器
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次操作的总代价

故n次操作代价是

,每次操作代价是

2.核算法

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

如果我们希望通过分析摊还代价来证明每个操作的平均代价的最坏情况很小,就应确保操作序列的总摊还代价给出了序列真实代价的上界。而且与聚合分析一样,这种关系必须对所有操作序列都成立。将第 i 个操作的摊还代价表示为

,真实代价表示为
,则要求
,数据结构中存储的信用恰好等于总摊还代价与总实际代价的差值,要求这一差值一直保持非负。比如在某个步骤,允许信用为负值(前面操作缴费不足,承诺在随后补齐账户欠费),那么当时的总摊还代价就小于总实际代价,对于到那个时刻为止的操作序列,总摊还代价就不再是总实际代价的上界了。因此,必须保证数据结构中的总信用永远为非负值。

还是回到上面的两个例子。

  • 栈操作

先考虑每个操作的实际代价(s是栈中元素个数):

然后我们赋予三个操作摊还代价:

假定使用¥1来代表1个单位的代价,那么每次PUSH时,除了支付¥1外,我们额外支付¥1作为可能发生的将来POP的预付费(信用),在POP时直接从信用里取,那么,任意时刻,每个栈里的元素都存在对应的预付费(信用)¥1,一定满足当前信用为非负值,即当前摊还代价是实际代价的一个上界。

因此,每次操作的摊还代价是

,n次操作的摊还代价是
  • 二进制计数器递增

我们设定置位(0变1)的摊还代价为2,复位(1变0)的摊还代价为0。

与上面的例子的分析方法相同,每次操作至多有1个位发生置位,这时除了支付¥1的实际代价之外,再预付将来可能发生的复位的代价¥1,那么将来复位时就可以直接从信用中拿。

因此,每次操作的摊还代价是

,n次操作的摊还代价是

3.势能分析

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

势能法工作方式如下。我们将对一个初始数据结构

执行 n 个操作。对每个 i = 1,2,......,n,令
为第i个操作的实际代价,令
为在数据结构
上执行第 i 个操作得到的结果数据结构。势能函
将每个数据结构 D
映射到一个实数
,此值即为关联到数据结构
的势能,第 i 个操作的摊还代价
用势函数
定义为:
。因此,第每个操作的摊还代价等于操作的实际代价加上此操作引起的势能变化。

累加即可得n步操作的总摊还代价:

如果能定义一个势函数

,使得对于任意的n都有:
,则总摊还代价
给出了总实际代价
的一个上界。

因此问题的关键在于定义一个合适的势函数

  • 栈操作

我们定义势函数为当前栈中的元素个数,那么

,且对于任意i,有
,那么任意时刻摊还代价都是实际代价的一个上界。

下面分析每个操作的摊还代价。

假设第i个操作是PUSH,且当时栈中包含s个对象,那么

,PUSH操作的摊还代价是

假设第i个操作是MULTIPOP(S,k),将

个对象弹出栈,对象的实际代价为
,势差为
,那么该操作的弹簧代价为

类似地,普通的POP操作的弹簧代价也为0。

因此,每次操作的摊还代价是

,n次操作的摊还代价是
  • 二进制计数器递增

我们定义第i次操作后,势函数为当前计数器中1的个数

假设第i次操作把

个位复位(1变回0),则实际代价至多位
,如果
,则所有k位都复位了,因此
,若干
,则
,无论哪种情况,都满足
,势差为

因此,摊还代价为

因此,每次操作的摊还代价是

,n次操作的摊还代价是

另外,即使计数器不是从0开始也可以使用势能法分析。

初始时有

个1,n次操作以后有
个1,把摊还代价的定义式移项,可得
,由于
,因此只要
,总实际代价就是
,即至少执行
次即可。

因此,不管计数器初值是什么,n次操作的总实际代价都是

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值