为什么我们需要摊还分析

上篇文章我们提到了算法的时间复杂度分析,给定输入规模,我们分析出算法的耗时,但是这样够了吗?

有时输入规模不是一个静态的值,可能输入是一系列操作,比如在这棵树里先插入结点,再做一个查找,再删除最小值,再与另一棵树合并。(插入,查找,删除最小值,合并)就是一个输入的操作序列。

为了对这类操作序列耗时进行分析,我们引入了摊还分析:n个操作的总耗时除以n。

看到这儿你可能会想,这不还是要分析总时间,和时间复杂度的区别不就除以个n而已嘛。但是对于时间复杂度的分析,我们关心的是最坏情况,假设我们有100个操作A,耗时为1,1个操作B耗时为1000,重点是:如果这个大耗时的操作发生,那么它前一定要发生100个耗时小的操作,也就是必须发生100个A才会发生B。如果我有50个操作(无A),100个操作(无A),101个操作(无B),这时我们分析时间复杂度还要把这个大耗时操作分情况考虑进去。这种情况就是摊还分析大展身手的时候,它的思想就是把大操作的耗时分摊到前面的小铺垫上

摊还分析在分析数据结构的操作耗时上特别有用,接下来我们来看几个例子,他们都来自算法导论:

例1. 用一个列表来实现栈

栈是一种数据结构,它具有后进先出的性质,如果要把数据从栈里弹出去,后加进来的数会被先弹出去。举个例子,洗碗池就是一个栈,碗就是数据,大家吃完饭,按吃完的顺序把碗放入洗碗池里,一个一个地按顺序叠起来。最后放进来的碗会被第一个洗掉,后放进来的碗会被洗掉移出洗碗池,这就叫后进先出。(哈哈哈哈现实中洗碗不是这样的,谁会把它叠起来啊!)

尽管我们一般用的是链表来实现栈,但是这次就用用数组吧。。

目标:从长度为1的数组开始形成一个长度为n的栈。

我们现在有个数组,它的长度固定了,假设它长度为5,就是它能放5个数据,OK,我们前5个数据入栈没有任何问题,可是我想要第6个数据也入栈,怎么办?

假设1个数据入栈耗时为1。

我们创建一个新的数组,长度比5长1,然后把这6个数据入栈。这会耗时为6。

按这种方法:一次就小气地只新建比原来长1的数组,我们想从长度为1的数组开始,做n次入栈操作。因为每次入栈都要一次复制(栈的长度从1长到2,再长到3,…),我们会耗时1+2+3+4+…+n = Θ(n2),摊还分析的时间复杂度就是Θ(n2)/n = Θ(n)

还有一种更聪明的做法,当我们栈满时还要加入元素,我们将栈的长度翻倍,比如从m变到2m,这耗费时间m(这是在原栈上直接翻倍,不是弄一个新的栈出来,所以不会花费时间2m)。所以这种做法有两种操作:
1.翻倍操作,耗时为当前栈的长度
2.入栈操作,耗时为1

所以我们从长度为1的数组开始,入栈n个元素,会出现如下操作序列:(入栈,翻倍,入栈,翻倍,入栈×2,翻倍,入栈×4,翻倍,入栈×8,翻倍…)这个序列也是我们之前提到的,需要有足够多的入栈操作进行铺垫,才会进行耗时多的翻倍操作。

接下来对这种聪明的办法进行摊还分析:
翻倍操作总耗时为1+2+4+8+…+2logn< 2n = O(n)
入栈操作总耗时是n,因为有n个元素要入栈
所以它们总耗时是O(n)+O(n) = O(n)
那么它的摊还代价是O(n) / n = O(1)。

可以看到,进行同一个事情,如果算法不一样,他们的摊还分析会不一样,采用第二个聪明的方法,我们使得摊还到每个操作上的时间代价从O(n)变到了O(1)。

核算法与势能法

为何要引进这两个方法,因为有的时候总代价并不好算,我们需要一些更巧妙的方法来计算摊还代价。我们需要的找的摊还代价最后找的要是实际耗费时间的上界,因为我们关心的是最坏情况。

核算法和势能法其实是一个方法的两种角度,我们进行摊还分析其实核心就在于把耗时长的大操作付出的代价摊还给每个耗时短的小操作,让大家共同承担。

所以核算法就是,假设你有一个银行账户,你看着这个序列有了初步想法,每次操作来临时,你先存一笔固定的钱,钱的数目你之前已经想好了,可以是3,可以是n,可以是n2这个每次固定存的钱就是摊还代价,然后按操作实际耗时来扣账户里的钱。
这个存钱操作唯一要求就是你的银行账户一直不能为负,因为我们要找的是摊还代价的上界,我们存进去的钱只能比总实际耗时多。假设有一个操作序列,是100个耗时为1的小操作后跟着1个耗时200的大操作,你打算每次存3元进去,小操作只花1元钱,所以你每次银行账户里还剩2元,接下来又来了99个小操作,你发财了,账户里存了200元,恐怖的是耗时200的大操作接踵而至,由于之前的规定,你还是固定存3元进去去硬抗这次大操作,扣了200元后你的账户还剩3元,这是因为吃了之前的老本,你的账户还是正的(也就是摊还总代价还是实际代价的上界),所以摊还代价就是3,是常数时间。这就是摊还分析!固定存3元就代表了一种耗时代价均摊的思想。

关于势能法,也是一样的,只不过它关心的是每一次操作后状态的改变。首先我定义一个势函数,一开始势函数的值是0,之后进行操作都会改变势函数,具体怎么改变是跟我的势函数有关的。我们定义摊还代价如下所示:

c’i = ciii-1
c’i是摊还代价,ci是第i次实际操作的代价,Φii-1是第i次操作后势函数的变化,初始时势函数Φ0=0。
对两边进行从1到n的求和,得出总摊还代价=总时间代价+Φn0

为了使得总摊还代价>总时间代价,Φn>0。

也就是为了使得总摊还代价是总时间代价的上界,我们需要让势函数最后不能为负。就像过山车一样,有的操作是在蓄势,像过山车爬坡,有的操作在放势,像过山车下坡,但是总体来说不管怎么蓄势放势,势函数最后要高于水平线0。

核算法与势能法的难点

这两种方法还是比较巧妙的,但是需要比较强的分析能力和构造能力。核算法难就难在想出我固定存多少钱进银行账户,是存常数?存n元?还是logn元?
势能法难点在于势函数的构造,我需要一个合理的势函数,它的势变化(Φii-1)和那一次操作实际代价(ci)之间的关联最好能够清楚地表现出来,成为我们的摊还代价(c’i)。

接下来举例说明这两种方法的应用

例2. 二进制计数器

我们对一个二进制数从0开始进行每次加1的操作,每翻一位数耗时为1,比如0000+1->0001,我们把最后一位从0翻成了1,耗时1。0101+1->0110,翻了最后两位,耗时为2。
这一系列的加法操作的摊还代价是多少?

1.核算法
我们对每一次的加法操作都把它想成往银行账户里固定存2元,这样我们对于二进制中从0变到1的情况,除了支付它从0变到1的代价,还存好了它从1变到0的代价。比如0000+1->0001,我们账户还有1元,这1元是为了让最后一位1变成0的,
0001+1->0010,我们用第一步的1元偿还了最后一位1变成0,并且我们新投入了两元支付了倒数第二位的0->1,并且还剩余1元,这1元是为了支付倒数第二位的1->0的。
0010+1->0011,0011+1->0100…这个累加操作其实就是使0变成1,1变成0,每次最多只有1个0变成1,我们存进去的2元就是为了满足从0变成1的花费的,并且还存下了1元,已经考虑到了他以后从1变成0了。所以每一位的变化,我们的2元钱都有充分考虑到,我们的银行账户永远不会变负,所以2就是我们的摊还代价。

2.势能法
我们定义势函数 Φ为计数器中1的个数,从定义可知道我们的势函数永远不会为负数,并且Φ0 = 0,c’i = ciii-1 = 2,因为如果我们就是单纯地在末尾的0加上一个1,那么ci=1,Φii-1=1因为整个二进制多了1个1。如果因为我们的加1使得00100111…1(最后m个1)变成了00101000…0(最后m个0),那么ci=m+1因为翻了m+1个数,Φii-1=-(m-1)因为整个二进制计数器少了m-1个1,所以不管什么情况,c’i = ciii-1 = 2。

总结一下
1.引入摊还分析是为了衡量操作序列的平均耗时,因为有些情况耗时高的操作需要耗时低的操作来铺垫,割裂他们来分析时间复杂度没有意义,所以需要用摊还分析把他们合起来均分着来分析。

2.计算摊还代价有三种方法:根据定义来算,核算法,势能法。后两种方法的核心在于通过猜想和构造得到实际操作总代价的上界。

3.核算法强调了一个固定投入多少摊还代价,对于便宜和贵重的操作一视同仁,固定投入多少的决定是它的难点。

4.势能法强调的是每一次实际操作的代价和其引起的势函数变化之间的关系展现,因为我们定义的摊还代价是每一次实际操作的代价(ci)+其引起的势函数变化(Φii-1)。如何定义势函数是它的难点。

摊还分析,核算法与势能法相关推荐

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

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

  2. 算法导论笔记:17摊还分析

    在摊还分析中,通过求数据结构的一系列的操作的平均时间,来评价操作的代价.这样,即使这些操作中的某个单一操作的代价很高,也可以证明平均代价很低.摊还分析不涉及概率,它可以保证最坏情况下每个操作的平均性能 ...

  3. 算法笔记(二)——浅析最好、最坏、平均、均摊时间分析方法

    为了使时间复杂度评价方法在不同量级情况下,评价更为全面.更精确,于是又可分为以下四种评价方法: (一)最好情况时间复杂度: 即一个程序在最好情况下的时间复杂度,比如,找一个数组中的元素,第一次就找到元 ...

  4. 最好、最坏、平均、均摊时间复杂度分析

    1.最好.最坏.平均情况时间复杂度 有时候我们分析一段代码的时间复杂度时,并不能很直观的就得出结果,需要结合具体的场合来判断它的平均情况.下面来看一个栗子: /*** 找出给定数组中给定元素的位置,如 ...

  5. 算法设计与分析第5章 回溯法(一)【回溯法】

    第5章 回溯法 5.1 回溯法 1.回溯法的提出  有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法. 2. 问题的解空间 (1)问题的解向量:回溯法希望 ...

  6. 方根法公式_仓储管理笔记之库存分析法:ABC分析法、区域合并法......

    导读 国内有庞大的仓储物流从业人员队伍(根据中国物流与采购联合会的调查,2016年底我国物流从业人员5012万,是人员增速最快的行业),很多人只是想深入了解仓库从无序到有序,从源头开始应该如何管理.如 ...

  7. 数据分析(7)路径挖掘分析法 行为序列分析法

    在之前的文章里,我们聊了7种数据分析的方法,分别是对比分析法.多维度拆解法.漏斗观察法.分布分析法和用户留存分析法.用户画像分析法和归因查找法,这篇文章我们来聊聊常见的数据分析方法中的最后两个:路径挖 ...

  8. 仓储管理之计价方法——毛利率法、零售价法【售价金额核算法】、计划成本法

    存货的发出计价方法有先进先出法.加权平均法.个别计价法等.加权平均法又分为月末一次加权平均法和移动加权平均法. 此外,在实务中,为了管理的需要,企业通常还采用毛利率法.零售价法.计划成本法等来核算发出 ...

  9. 软件测试 通用技术03 测试用例 黑盒测试用例设计方法 等价类划分法 边界值分析法 判定表法 场景法 功能图法 其他用例设计方法 用例设计方法综合选择

    文章目录 1 测试用例 1.1 测试用例的定义 1.2 测试用例模板 1.3 测试用例模板的内容 测试用例编号 测试项 依赖用例 测试步骤 测试数据 预期结果 测试结果 测试人 备注 2 测试用例编写 ...

最新文章

  1. Mybatis源码解读-设计模式总结
  2. 2016级算法期末上机-F.中等·AlvinZH's Fight with DDLs II
  3. 【CEO赠书】《精益数据分析》:如何构建数据指标体系
  4. 为什么说选择正确的编程语言很重要,以及如何正确的选择
  5. android系统开发实验,基于Android智能手机的实验管理系统的设计与实现
  6. Javascript的websocket的使用方法
  7. eclipse导入jar包的三种方法
  8. SHA1withRSA加签名和验签名
  9. 计算机软件技术基础_「连载」信息技术基础题型归纳之计算机软件2
  10. 状态空间方程的等价问题
  11. C#窗体之整人小程序
  12. u深度重装系统详细教程_U盘怎样使用U深度给电脑装系统教程
  13. 沪牌学院-沪拍拍课堂2: 出价策略
  14. 解决Mac无法睡眠问题
  15. python 进阶案例_Python 进阶内容整理
  16. Egret做微信好友排行榜
  17. 使用C++实现FC红白机模拟器 Cartridge 与 Mapper(原理篇)
  18. 全球及中国智能家居市场十四五竞争形势及营销模式咨询报告2021-2027年
  19. 预印:提前出版研究发表有负面影响吗?
  20. 吕蒙正千年奇文《寒窑赋》鉴赏

热门文章

  1. python无法加载文件系统代码_致命的Python错误:initfsencoding:无法加载文件系统cod...
  2. matlab判断矩阵是否非负,有关非负矩阵的MATLAB程序优化
  3. slot、slot-scope与v-slot替换
  4. iOS 14.2 Beta为Control Center添加了新的Shazam音乐识别功能
  5. ubuntu16.04更换清华源
  6. 计算机argument,编程中argument什么意思?一定要准确..
  7. JVM-01-JVM知识
  8. 电脑版微信dat文件用什么软件打开
  9. dogepool.pw index.php,php – 在Dogecoin转换欧元
  10. 前端学习笔记——JavaScript进阶