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))

  • 相关阅读:
    从自然数到有理数
    付费版乐影音下载器使用方法
    Avtiviti之流程变量
    activity(工作流)初步学习记录
    IntelliJ IDEA安装Activiti插件并使用
    golang 性能测试
    Golang性能测试工具PProf应用详解
    java连接ZK的基本操作
    会员体系、积分、等级
    Flink基本概念
  • 原文地址:https://www.cnblogs.com/dongkuo/p/5824007.html
Copyright © 2011-2022 走看看