用外行的术语来分摊复杂性?

有人能用门外汉的话来解释分摊复杂性吗?我一直很难在网上找到一个精确的定义,我不知道它完全与算法分析有关。任何有用的信息,即使是外部引用,我们也会非常感激。

41266 次浏览

“分摊复杂性”的原则是,尽管当你做某事时可能相当复杂,因为它不经常做,它被认为“不复杂”。例如,如果你创建一个二叉树,需要时不时地进行平衡——比如每次 2^n插入一次——因为尽管平衡树非常复杂,但是每 n 次插入只会发生一次(例如,在插入号256处平衡一次,然后在512、1024等处平衡一次)。在所有其他的插入中,复杂度是 O (1)-是的,每 n 次插入都需要 O (n) ,但这只是 1/n概率-所以我们将 O (n)乘以1/n 得到 O (1)。这就是所谓的“ O (1)的摊销复杂度”——因为当你添加更多的元素时,重新平衡树所花费的时间是最小的。

在重复运行中分摊的平均值。最坏情况下的行为保证不会发生得太频繁。例如,如果最慢的情况是 O (N) ,但发生的几率只是 O (1/N) ,否则进程是 O (1) ,那么算法仍然会有分摊常数 O (1)时间。只要考虑每个 O (N)运行的工作被分配到 N 个其他运行。

这个概念取决于有足够的运行时间来划分总时间。如果算法只运行一次,或者每次运行都必须满足一个最后期限,那么最坏情况下的复杂度就更相关。

这有点类似于将算法中不同分支的最坏情况复杂度乘以执行该分支的概率,然后添加结果。因此,如果一些分支是非常不可能采取的,它贡献较少的复杂性。

摊销复杂度是每次操作的总费用,通过一系列操作进行评估。

这个想法是为了保证整个序列的总费用,同时允许个别操作比摊销成本昂贵得多。

例如:
C + + std::vector<>的行为。当 push_back()增加向量大小超过其预分配值时,它使分配的长度增加一倍。

因此,单个 push_back()可能需要 O(N)时间来执行(因为数组的内容被复制到新的内存分配)。

但是,因为分配的大小增加了一倍,所以下一次对 push_back()N-1调用每次执行都要花费 O(1)的时间。因此,总的 N操作仍然需要 O(N)时间,从而使 push_back()的摊销成本为每次操作 O(1)


除非另有说明,摊销复杂度是任何操作序列的渐近最坏情况保证。这意味着:

  • 与非摊销复杂性一样,用于摊销复杂性的大 O 表示法忽略了固定的初始开销和固定的性能因素。因此,为了评估大 O 分期付款的性能,你通常可以假设任何分期付款的操作序列将“足够长”,以分期付清固定的启动费用。具体地说,对于 std::vector<>示例,这就是为什么您不需要担心是否会遇到 N附加操作: 分析的渐近性质已经假定您会遇到。

  • 除了任意长度之外,平摊分析不会对你所衡量的操作顺序做出假设——这是对 任何可能的序列操作的最坏保证。无论操作选择得多么糟糕(比如,被恶意的对手选中!)平摊分析必须保证一个足够长的操作序列的成本不会一直高于其摊销成本的总和。这就是为什么“概率”和“平均情况”与平摊分析无关——就像它们与普通的最坏情况“大 O”分析无关一样!

在一个平摊分析中,执行一系列数据结构操作所需的时间是对所有执行的操作进行平均的... ... 平摊分析与平均情况分析的不同之处在于不涉及概率,一个平摊分析保证了在最坏的情况下每个操作的平均性能。

(摘自 Cormen 等人的《算法入门》)

这可能有点令人困惑,因为它既说时间是平均的,又说它不是一个平均情况下的分析。因此,让我试着用一个金融类比来解释这个问题(事实上,“摊销”一词最常与银行和会计联系在一起)

假设您正在操作彩票。(不是买彩票,稍后我们会讲到,而是操作彩票本身。)你打印100,000张彩票,每张售价一个货币单位。其中一张票可以让买家获得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.

现在,我声明 putget的摊销成本是 O(1),假设我以一个空队列开始和结束。分析很简单: 我总是将 put放在右侧堆栈上,将 get放在左侧堆栈上。因此,除了 If子句之外,每个 put都是 push,每个 get都是 pop,它们都是 O(1)。我不知道执行 If子句多少次——这取决于 putget的模式——但是我知道每个元素从右边堆栈到左边堆栈只移动一次。因此,在整个序列的 n 个 put和 n 个 get上的总成本是: n 个 pushes,n 个 pops 和 n 个 get8s,其中 get8是一个 pop,接着是一个 push: 换句话说,2n 个操作(n 个 put和 n 个 gets)导致2n 个 pushes 和2n 个 pops。所以单个 putget的摊销成本是一个 push和一个 pop

请注意,银行家队列之所以被称为银行家队列,正是因为摊销复杂性分析(以及“摊销”这个词与金融的关联)。银行家的队列是过去常见的面试问题的答案,尽管我认为它现在被认为太有名了: 想出一个队列,在分摊的 O (1)时间内实现以下三个操作:

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

2)在队列中添加一个新元素,

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

假设您试图找到一个未排序数组的第 kth 最小元素。 对数组进行排序将是 O (n logn)。 那么找到 kth 最小数就是定位索引,也就是 O (1)。

因为数组已经排序了,所以我们再也不用排序了。最坏的情况我们不会遇到超过一次。

如果我们执行 n 个查询,尝试定位 kth 最小值,它仍然是 O (n logn) ,因为它优于 O (1)。如果我们把每次操作的时间平均下来,结果会是:

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

这是分摊复杂性。

我想这是怎么回事,我只是学习它太。。