用外行的术语摊销复杂性?


75

有人可以用外行的术语解释摊销的复杂性吗?我一直很难在网上找到精确的定义,而且我不知道它与算法分析完全相关。任何有用的东西,即使从外部引用,也将受到高度赞赏。




Answers:


98

摊销的复杂度是在一系列操作中评估的每次操作的总费用。

想法是保证整个序列的总费用,同时允许单个操作的成本比摊余成本高得多。

示例:
C ++的行为std::vector<>。当push_back()向量大小增加到其预先分配的值以上时,它将使分配的长度加倍。

因此执行单个操作push_back()可能会花费一些O(N)时间(因为将数组的内容复制到新的内存分配中)。

但是,由于分配的大小增加了一倍,因此对下一个的N-1调用push_back()将分别花费一些O(1)时间来执行。因此,整个N操作仍需要O(N)时间。从而push_back()得出O(1)每笔业务的摊销成本。


除非另有说明,否则分摊的复杂度是任何操作序列的渐近最坏情况保证。这表示:

  • 与未摊销的复杂度一样,用于摊销的复杂度的big-O符号忽略固定的初始开销和恒定的性能因子。因此,出于评估big-O摊销性能的目的,通常可以假定摊销操作的任何顺序将“足够长”以摊销固定的启动费用。具体来说,对于std::vector<>示例而言,这就是为什么您无需担心您是否会真正遇到N其他操作的原因:分析的渐近性质已经假定您会这​​样做。

  • 除任意长度外,摊销分析不会对您要衡量其成本的工序顺序进行假设-这是对任何可能工序顺序的最坏情况保证。无论选择的操作有多糟糕(例如,恶意对手!),摊销分析都必须确保足够长的操作序列所花费的费用可能不会始终超过摊销费用之和。这就是为什么(除非特别提到,作为限定词)“概率”和“平均情况”与摊销分析不相关-与普通的最坏情况的big-O分析无关!


31

在摊销分析中,执行一系列数据结构操作所需的时间是在所有已执行的操作中平均的。摊销分析可确保在最坏的情况下每个操作的平均性能。

(摘自Cormen等人,“算法简介”)

这可能会造成一些混淆,因为它表示时间是平均时间,而不是平均情况分析。因此,让我尝试用财务类比来解释这一点(实际上,“摊销”是与银行和会计最相关的一个词。)

假设您正在操作彩票。(不买彩票,我们稍后会讲,而是自己操作彩票。)您打印100,000张彩票,每张将以1个货币单位出售。其中一张票将使购买者有权使用40,000个货币单位。

现在,假设您可以卖掉所有门票,您将获得60,000货币单位:100,000货币单位的销售额减去40,000货币单位的奖励。对于您来说,每张票证的价值为0.60货币单位,在所有票证中摊销。这是一个可靠的值;你可以依靠它。如果您厌倦了自己卖票,而有人进来并提出以每张0.30货币单位的价格出售票,您将确切知道自己的立场。

对于彩票购买者,情况有所不同。购买者在购买彩票时预期会损失0.60货币单位。但这是概率性的:购买者可能在30年内每天购买10张彩票(有点超过100,000张彩票)而从未中奖。或者他们可能有一天自发地购买一张票,并赢得39,999个货币单位。

应用于数据结构分析时,我们谈论的是第一种情况,即在所有此类操作中分摊某些数据结构操作(例如插入)的成本。平均案例分析处理的是随机操作(例如搜索)的期望值,在这里我们无法计算所有操作的总成本,但可以对单个操作的期望成本进行概率分析。

人们常说,摊销分析适用于很少有高成本操作的情况,通常就是这种情况。但不总是。例如,考虑所谓的“银行家队列”,它是由两个堆栈组成的先进先出(FIFO)队列。(这是一个经典的功能数据结构;您可以在不可变的单链接节点上构建便宜的LIFO堆栈,但是便宜的FIFO并不是那么明显)。具体实现如下:

put(x):  Push x on the right-hand stack.
y=get(): If the left-hand stack is empty:
           Pop each element off the right-hand stack and
             push it onto the left-hand stack. This effectively
             reverses the right-hand stack onto the left-hand stack.
         Pop and return the top element of the left-hand stack.

现在,我假设put和的摊销成本getO(1),假设我以空队列开始和结束。分析很简单:我总是从左侧put堆栈进入右侧get堆栈。因此,除了该If子句外,每个put都是a push,每个get都是a pop,两个都是O(1)。我不知道我将执行该If子句多少次-它取决于puts和gets的模式-但我知道每个元素从右手堆栈到左手堆栈都只能移动一次。因此,整个nsput和ns序列的总成本get为:pushns,nspop和ns move,其中amove是apop后跟a push:换句话说,2n次运算(n puts和n gets)导致2n pushes和2n pops。因此,一个putget一个的摊销成本为一push加一pop

请注意,之所以这么称呼银行家,是因为进行了摊销的复杂性分析(以及“摊销”一词与财务的关联)。银行家的队列是对曾经是一个常见面试问题的答案,尽管我认为它现在已经众所周知:提出一个在摊销的O(1)时间内实现以下三个操作的队列:

1)获取并删除队列中最早的元素,

2)将一个新元素放入队列,

3)找到当前最大元素的值。


22

“摊余复杂性”的原则是,尽管某些事情在您执行时可能会非常复杂,但由于它并不经常执行,因此被视为“不复杂”。例如,如果您创建一个需要不时进行2^n平衡的二叉树(例如,每个插入一次),因为尽管平衡该树非常复杂,但它仅每n次插入发生一次(例如,在插入编号256处一次,然后在第512、1024等)。在所有其他插入上,复杂度为O(1)-是的,每n次插入需要O(n)一次,但这只是1/n概率-因此我们将O(n)乘以1 / n并得到O(1)。因此,这被称为“ O(1)的摊余复杂度”-因为当您添加更多元素时,重新平衡树所花费的时间最少。


1
“足够大”与它有什么关系?这里有一些无关紧要的细节,并且省略了乘以概率的关键概念。
Potatoswatter

不合格的摊销性能保证与概率无关-它们是任何操作顺序的绝对保证。在谈论概率性能时,应使用表示“预期”或“平均情况”性能的技术术语。
13年

我已经对其进行了重新修饰,以删除多余的“足够大”(这意味着一棵小树很可能会很频繁地重新平衡,但是一棵大树却不会经常重新平衡-但我同意这并不是一种很好的表达方式,因为随着努力的增加,努力也会增加)
Mats Petersson

摊余的复杂度与平均情况下的复杂度不同,因此,正如@comingstorm所说,概率不包含在内。要将摊销分析应用于重新平衡二叉树,您必须证明(最坏情况)重新平衡为每个插入贡献了恒定的时间。
rici

@comingstorm谢谢,我已经确定了答案。
Potatoswatter

5

摊销均值除以重复运行。保证最坏情况下的行为不会频繁发生。例如,如果最慢的情况是O(N),但是发生这种情况的机会仅为O(1 / N),否则该过程是O(1),则算法仍将具有摊销常数O(1)时间。只需考虑将每个O(N)运行的工作分解为N个其他运行。

该概念取决于是否有足够的运行来划分总时间。如果算法仅运行一次,或者每次运行都必须满足最后期限,那么最坏情况下的复杂性就更重要。


4

假设您要查找未排序数组的第k个最小元素。将数组排序为O(n logn)。因此,找到第k个最小数字就是对索引进行定位,所以O(1)。

由于数组已经排序,因此我们不必再次排序。我们绝不会遇到最坏的情况。

如果我们执行n次尝试试图找到第k个最小的查询,则它仍将是O(n logn),因为它在O(1)上占主导地位。如果我们平均每次操作的时间为:

(n logn)/ n或O(logn)。因此,时间复杂度/操作数。

这是摊销的复杂性。

我认为这是前进的方向,我也只是在学习它。


2

这有点类似于将算法中不同分支的最坏情况复杂度乘以执行该分支的可能性,然后相加结果。因此,如果某个分支极不可能采用,那么它对复杂性的贡献就较小。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.