有人可以用外行的术语解释摊销的复杂性吗?我一直很难在网上找到精确的定义,而且我不知道它与算法分析完全相关。任何有用的东西,即使从外部引用,也将受到高度赞赏。
有人可以用外行的术语解释摊销的复杂性吗?我一直很难在网上找到精确的定义,而且我不知道它与算法分析完全相关。任何有用的东西,即使从外部引用,也将受到高度赞赏。
Answers:
摊销的复杂度是在一系列操作中评估的每次操作的总费用。
想法是保证整个序列的总费用,同时允许单个操作的成本比摊余成本高得多。
示例:
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分析无关!
在摊销分析中,执行一系列数据结构操作所需的时间是在所有已执行的操作中平均的。摊销分析可确保在最坏的情况下每个操作的平均性能。
(摘自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
和的摊销成本get
为O(1)
,假设我以空队列开始和结束。分析很简单:我总是从左侧put
堆栈进入右侧get
堆栈。因此,除了该If
子句外,每个put
都是a push
,每个get
都是a pop
,两个都是O(1)
。我不知道我将执行该If
子句多少次-它取决于put
s和get
s的模式-但我知道每个元素从右手堆栈到左手堆栈都只能移动一次。因此,整个nsput
和ns序列的总成本get
为:push
ns,nspop
和ns move
,其中amove
是apop
后跟a push
:换句话说,2n次运算(n put
s和n get
s)导致2n push
es和2n pop
s。因此,一个put
或get
一个的摊销成本为一push
加一pop
。
请注意,之所以这么称呼银行家,是因为进行了摊销的复杂性分析(以及“摊销”一词与财务的关联)。银行家的队列是对曾经是一个常见面试问题的答案,尽管我认为它现在已经众所周知:提出一个在摊销的O(1)时间内实现以下三个操作的队列:
1)获取并删除队列中最早的元素,
2)将一个新元素放入队列,
3)找到当前最大元素的值。
“摊余复杂性”的原则是,尽管某些事情在您执行时可能会非常复杂,但由于它并不经常执行,因此被视为“不复杂”。例如,如果您创建一个需要不时进行2^n
平衡的二叉树(例如,每个插入一次),因为尽管平衡该树非常复杂,但它仅每n次插入发生一次(例如,在插入编号256处一次,然后在第512、1024等)。在所有其他插入上,复杂度为O(1)-是的,每n次插入需要O(n)一次,但这只是1/n
概率-因此我们将O(n)乘以1 / n并得到O(1)。因此,这被称为“ O(1)的摊余复杂度”-因为当您添加更多元素时,重新平衡树所花费的时间最少。