zoukankan      html  css  js  c++  java
  • 摊还分析(1)——算法导论(23)

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

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

    1. 聚合分析

    1.1 栈操作

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

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

    while not stack-empty(S) and k > 0
    	pop(s)
        k = k - 1
    

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

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

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

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

    1.2 二进制计数器递增

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

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

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

    下面是python实现代码:

    def increment(num):
        i = 0
        while i < len(num) and num[i] == 1:
            num[i] = 0
            i += 1
        if i < len(num):
            num[i] = 1
    

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

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

    [ sum_{i=0}^{k-1}⌊frac{n}{2^i}⌋ < nsum_{i=0}^{infty } frac{1}{2^i}< 2 n ]

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

    2. 核算法

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

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

    2.1 栈操作

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

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

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

    操作 摊还代价
    push 2
    pop 0
    multipop 0

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

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

    这是因为(sumlimits_{i=1}^{n}c'_i geq sumlimits_{i=1}^{n}c_i)在任何时刻都成立,其中(c_i)为第i个操作的真实代价,(c_i')为其摊还代价。

    2.2 二进制计数器递增

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

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

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

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

    3. 势能法

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

    假设我们对一个初始数据结构(D_0)执行n个操作。对每一个(i=1, 2,...,n),用(c_i)表示第i个操作的实际代价,(D_i)表示在数据结构(D_{i-1})上执行第i个操作得到的结果数据结构。势函数(Theta)将每个数据结构(D_i)映射到一个实数(Theta(D_i)),此值即为关联到数据结构(D_i)的势;并且定义第i个操作的摊还代价(c_i')为:

    [c_i' = c_i + Theta(D_i) - Theta(D_{i-1}) ]

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

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

    [sum_{i = 1}^n c_i' = sum_{i = 1}^n(c_i + Theta(D_i) - Theta(D_{i-1})) = sum_{i=1}^nc_i + Theta(D_n) - Theta(D_0) ]

    做一个移项,变形得:

    [sum_{i = 1}^n c_i' - sum_{i = 1}^n c_i = Theta(D_n) - Theta(D_0) ]

    如果我们能定义一个势函数(Theta),使得(Theta(D_n) geq Theta(D_0)),则总摊还代价(sum_limits{i = 1}^n c_i')给出了总实际代价(sum_limits{i = 1}^n c_i)的一个上界。

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

    3.1 栈操作

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

    对于势函数的选取,我们选择将栈映射到其内部元素个数的函数,即(Theta(D_i)表示第)(i)次操作栈时,栈中元素的个数。对于初始的空栈,有(Theta(D_0) = 0);并且由于栈中元素不可能为负,因此(Theta(D_i) geq 0 = Theta(D_0))

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

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

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

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

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

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

    3.2 二进制计数器递增

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

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

    再分析总摊还代价。对于一次increment操作,其实际代价为(k')次复位操作和1次置位操作,为(k'+1);其引起的势差变化为(-k'+1),因此摊还代价为((k'+1) + (-k' + 1) = 2)(n)个操作的总摊还代价为(O(n)),因此总实际代价最坏为(O(n))

  • 相关阅读:
    Asp.Net Web API 2第八课——Web API 2中的属性路由
    Asp.Net Web API 2第七课——Web API异常处理
    Asp.Net Web API 2第六课——Web API路由和动作选择
    Asp.Net Web API 2第五课——Web API路由
    开始学习python
    BMI 小程序 购物车
    深浅copy 文件操作
    字典 dict 集合set
    基本数据类型 (str,int,bool,tuple,)
    python 运算符
  • 原文地址:https://www.cnblogs.com/dongkuo/p/5824007.html
Copyright © 2011-2022 走看看