恒定摊销时间

Posted

技术标签:

【中文标题】恒定摊销时间【英文标题】:Constant Amortized Time 【发布时间】:2010-09-17 01:19:26 【问题描述】:

在谈到算法的时间复杂度时,“”是什么意思?

【问题讨论】:

mortoray.com/2014/08/11/what-is-amortized-time 【参考方案1】:

简述摊销时间:

如果您进行一百万次操作,您并不真正关心该操作的最坏情况或最佳情况 - 您关心的是当您重复该操作时总共花费了多少时间一百万次。

因此,操作是否偶尔非常缓慢并不重要,只要“偶尔”足够罕见,可以将缓慢冲淡掉。基本上摊销时间是指“每次操作所花费的平均时间,如果你做了很多操作”。摊销时间不必是恒定的;你可以有线性和对数摊销时间或其他任何东西。

让我们以 mats 的动态数组为例,您可以在其中重复添加新项目。通常添加一个项目需要固定的时间(即O(1))。但每次阵列满时,您都会分配两倍的空间,将数据复制到新区域,并释放旧空间。假设 allocate 和 frees 以恒定时间运行,这个扩大过程需要 O(n) 时间,其中 n 是数组的当前大小。

因此,每次放大时,您花费的时间大约是上次放大的两倍。但你也等了两倍的时间才这样做!因此,每次扩大的成本可以在插入之间“分摊”。这意味着从长远来看,将 m 项添加到数组中所用的总时间为O(m),因此摊销时间(即每次插入的时间)为O(1)

【讨论】:

只是符号方面的说明:O(n) 的摊销常数执行时间通常写为 O(n)+,而不仅仅是 O(n)。添加加号表示不保证执行时间为 O(n),实际上可以超过该执行时间。 在分配空间方面,是来自堆吗? 我不同意“你并不真正关心最坏的情况”。这取决于用例。如果最终您只对引用的 100 万次操作的结果感兴趣,那么您确实不在乎。但是,如果它是一个实时应用程序,它会不断读取数据然后对其进行响应,那么您可能会遇到一个大问题,如果每处理 100 万个数据项,处理该数据所需的时间是正常情况的 100 万倍! @Jeffpowrs 我想that O(n) was linear time and O(1) was constant time。那么这是否意味着 O(1)+ 将被摊销恒定时间而 O(n)+ 将被摊销 linear 时间? @JohnMeyer 是的。【参考方案2】:

这意味着随着时间的推移,最坏的情况将默认为 O(1) 或恒定时间。一个常见的例子是动态数组。如果我们已经为一个新条目分配了内存,那么添加它将是 O(1)。如果我们还没有分配它,我们将通过分配,比如说,当前数量的两倍来分配它。这个特定的插入将不是是 O(1),而是其他的东西。

重要的是该算法保证在一系列操作之后,昂贵的操作将被摊销,从而呈现整个操作 O(1)。

或者更严格地说,

有一个常数 c,因此对于 每一个操作序列(也有一个以代价高昂的操作结束) 长度L,时间不大于 c*L (感谢Rafał Dowgird)

【讨论】:

“经过足够多的操作” - 恒定摊销时间不需要这个条件。有一个常数 c,因此对于长度为 L 的 每个 操作序列(也有一个以代价高昂的操作结束),时间不大于 c*L。 这个分配两倍的金额从何而来?我们不应该分配一个条目吗?或者这是一个假设的例子? @talekeDskobaDa 这不是一个随意的例子,而是一个广泛使用的算法。如果我们按照您的建议一次为一个条目分配空间,则插入单个值的摊销时间将为 O(n)。如果我们在空间满时将其翻倍,则摊销时间要好得多,O(1)。需要明确的是,一次为一个项目分配空间的问题是数组需要一大块连续的空间。从操作系统获取更大的块很容易,但扩展现有块通常是不可能的,因为它后面可能直接存储了一些其他数据。【参考方案3】:

要开发一种直观的思考方式,请考虑在 dynamic array 中插入元素(例如 C++ 中的 std::vector)。让我们绘制一个图表,显示在数组中插入 N 个元素所需的操作数 (Y) 的依赖关系:

黑色图形的垂直部分对应于内存的重新分配以扩展数组。在这里我们可以看到,这种依赖关系可以粗略地表示为一条线。而这条线方程是Y=C*N + bC 是常数,在我们的例子中b = 0)。因此我们可以说我们平均需要花费C*N操作来添加N个元素到数组中,或者C*1操作来添加一个元素(摊销常数时间)。

【讨论】:

为什么分配之间存在斜率?那不应该是水平的来描述所花费的恒定时间吗?【参考方案4】:

重复阅读3次后,我发现下面的***解释很有用:

来源:https://en.wikipedia.org/wiki/Amortized_analysis#Dynamic_Array

"动态数组

动态数组推送操作的摊销分析

考虑一个动态数组,它的大小会随着添加更多元素而增长 例如 Java 中的 ArrayList。如果我们从动态数组开始 大小为 4 时,将四个元素推到它上面需要恒定的时间。 然而,将第五个元素推入该数组将花费更长的时间,因为 array 必须创建一个两倍于当前大小 (8) 的新数组, 将旧元素复制到新数组中,然后添加新元素 元素。接下来的三个推送操作将同样采用常量 时间,然后随后的添加将需要另一个缓慢的 数组大小翻倍。

一般来说,如果我们考虑将任意数量的推入 n 到一个数组 大小为 n,我们注意到推送操作需要恒定的时间,除了 对于最后一个需要 O(n) 时间来执行大小加倍的 手术。由于总共有 n 次操作,我们可以取平均值 并找到将元素推入动态数组的方法 需要:O(n/n)=O(1),恒定时间。”

我的理解是一个简单的故事:

假设你有很多钱。 而且,你想把它们堆放在一个房间里。 而且,您的手和腿很长,只要您现在或将来需要。 而且,你必须把所有东西都填在一个房间里,所以很容易把它锁起来。

所以,你直接走到房间的尽头/角落并开始堆叠它们。 当你堆叠它们时,房间会慢慢地用完空间。 但是,当您填充时,很容易将它们堆叠起来。拿到钱,放钱。简单。它是 O(1)。我们不需要转移任何以前的资金。

一旦房间空间不足。 我们需要另一个更大的房间。 这里有一个问题,因为我们只能有 1 个房间,所以我们只能有 1 个锁,我们需要将那个房间里所有现有的钱转移到新的更大的房间里。 所以,把所有的钱,从小房间转移到大房间。即,再次将它们全部堆叠。 所以,我们确实需要转移所有以前的钱。 因此,它是 O(N)。 (假设 N 是前钱的总钱数)

换句话说,很容易直到 N,只有 1 次操作,但是当我们需要搬到更大的房间时,我们做了 N 次操作。所以,换句话说,如果我们平均下来,它是在开始时插入 1 次,然后在移动到另一个房间时再移动 1 次。 总共 2 次操作,一次插入,一次移动。

假设即使在小房间里 N 也大到 100 万,那么 2 次操作与 N(100 万)相比并不是一个真正的可比数字,因此它被认为是常数或 O(1)。

假设当我们在另一个更大的房间里做以上所有事情时,又需要搬家。 它仍然是一样的。 比方说,N2(比方说,10 亿)是更大房间里新的钱数

所以,我们有 N2(包括以前的 N 个,因为我们将所有房间都从小房间搬到了大房间)

我们仍然只需要两个操作,一个是插入到更大的房间,然后另一个移动操作来移动到一个更大的房间。

因此,即使对于 N2(10 亿),每个操作也是 2 次。这又没什么了。所以,它是常数,或者 O(1)

因此,当 N 从 N 增加到 N2 或其他时,这并不重要。它仍然是恒定的,或者 N 中的每一个都需要 O(1) 次操作。


现在假设,你有 N 为 1,非常小,点的钱也很少,你有一个很小的房间,只能放 1 点钱。

只要你把钱填在房间里,房间就被填满了。

当你去更大的房间时,假设它只能再装一个钱,总共 2 个钱。这意味着,前一个移动的钱和另外 1 个。又被填满了。

这样,N 增长缓慢,不再是常数 O(1),因为我们将所有钱从前一个房间转移,但只能再容纳 1 个钱。

100 次后,新房间可以容纳 100 元以前的钱,还能容纳 1 多钱。这是O(N),因为O(N+1)就是O(N),也就是100和101的度数相同,都是百,而不是之前的故事,一到百万,一到十亿.

因此,这是为我们的钱(变量)分配房间(或内存/RAM)的低效方式。


所以,一个好的方法是分配更多的空间,2 的幂。

第一个房间大小 = 适合 1 个钱 第二个房间大小 = 可容纳 4 个钱 第 3 间房间大小 = 可容纳 8 个钱 第 4 个房间大小 = 可容纳 16 个钱 第 5 间房间大小 = 可容纳 32 个钱 第 6 间房间大小 = 可容纳 64 点钱 第 7 间房间大小 = 可容纳 128 块钱 第 8 间房间大小 = 可容纳 256 个钱 第 9 间房间大小 = 可容纳 512 个钱 第 10 间房间大小 = 可容纳 1024 个钱 第 11 个房间大小 = 可容纳 2,048 个钱 ... 第 16 间房间大小 = 可容纳 65,536 个钱 ... 第 32 间房间大小 = 可容纳 4,294,967,296 个钱 ... 第 64 间房间大小 = 可容纳 18,446,744,073,709,551,616 笔钱

为什么这样更好?因为它看起来一开始增长缓慢,后来增长更快,也就是说,与我们 RAM 中的内存量相比。

这很有帮助,因为在第一种情况下虽然很好,但每笔钱要完成的总工作量是固定的 (2),并且无法与房间大小 (N) 相比,即我们在初始阶段使用的房间可能太大(100 万)我们可能无法完全使用,这取决于我们是否可以在第一种情况下节省那么多钱。

但是,在最后一种情况下,2 的幂,它会在我们 RAM 的限制中增长。因此,随着 2 次方的增加,Armotized 分析保持不变,并且对于我们今天拥有的有限 RAM 是友好的。

【讨论】:

啊,所以它是 O(最坏情况 / 操作数)。我最喜欢这个答案。【参考方案5】:

我创建了这个简单的 python 脚本来演示 python 列表中追加操作的分摊复杂性。我们不断向列表中添加元素并为每个操作计时。在这个过程中,我们注意到一些特定的追加操作需要更长的时间。这些峰值是由于正在执行新的内存分配。需要注意的重要一点是,随着追加操作数量的增加,尖峰会变得更高,但间距会增加。间距的增加是因为每次初始内存遇到溢出时都会保留更大的内存(通常是之前的两倍)。希望这会有所帮助,我可以根据建议进一步改进它。

import matplotlib.pyplot as plt
import time


a = []
N = 1000000

totalTimeList = [0]*N
timeForThisIterationList = [0]*N
for i in range(1, N):
    startTime = time.time()
    a.append([0]*500) # every iteartion, we append a value(which is a list so that it takes more time)
    timeForThisIterationList[i] = time.time() - startTime
    totalTimeList[i] = totalTimeList[i-1] + timeForThisIterationList[i]
max_1 = max(totalTimeList)
max_2 = max(timeForThisIterationList)

plt.plot(totalTimeList, label='cumulative time')
plt.plot(timeForThisIterationList, label='time taken per append')
plt.legend()
plt.title('List-append time per operation showing amortised linear complexity')
plt.show()

这会产生以下情节

【讨论】:

每个附加行所花费的时间非常明确【参考方案6】:

上述解释适用于聚合分析,即对多个操作进行“平均”的想法。 我不确定它们如何应用于银行家方法或物理学家摊销分析方法。

现在。我不确定正确的答案。 但这与物理学家+银行家两种方法的主要条件有关:

(摊销运营成本总和)>=(实际运营成本总和)。

我面临的主要困难是,鉴于运营的摊销渐近成本不同于正常渐近成本,我不确定如何评估摊销成本的重要性。

当有人给我一个摊销成本时,我知道它与正常渐近成本不同那我从摊销成本中得出什么结论?

由于我们遇到了一些操作被多收而其他操作被低估的情况,一个假设可能是引用单个操作的摊销成本将毫无意义。

例如:对于斐波那契堆,仅将 Decreating-Key 的摊销成本引用为 O(1) 是没有意义的,因为“早期操作在增加堆潜力方面所做的工作”会降低成本。

我们可以有另一个假设来解释摊销成本的原因如下:

    我知道昂贵的操作将在多个低成本操作之前进行。

    为了便于分析,我将对一些低成本操作收取过高的费用,这样它们的渐近成本不会改变。

    通过这些增加的低成本操作,我可以证明昂贵的操作具有更小的渐近成本。

    因此,我改进/降低了 n 次操作成本的 ASYMPTOTIC-BOUND。

因此,摊销成本分析 + 摊销成本界限现在仅适用于昂贵的操作。廉价操作的渐近摊销成本与其正常渐近成本相同。

【讨论】:

有趣的想法。【参考方案7】:

任何函数的性能可以通过将“函数调用总数”除以“所有这些调用所花费的总时间”来平均。即使是每次调用花费越来越长的函数,仍然可以通过这种方式进行平均。

因此,在Constant Amortized Time 处执行的函数的本质是,这个“平均时间”达到了一个上限,随着调用次数的不断增加,这个上限不会被超过。任何特定调用的性能可能会有所不同,但从长远来看,这个平均时间不会越来越长。

这是在Constant Amortized Time 执行的某些东西的基本优点。

【讨论】:

【参考方案8】:

摊销运行时间: 这是指根据每次操作使用的时间或内存计算算法复杂度。 它用于大多数情况下操作很快但在某些情况下算法的操作很慢。 因此,研究操作序列以了解更多关于摊销时间的信息。

【讨论】:

以上是关于恒定摊销时间的主要内容,如果未能解决你的问题,请参考以下文章

unordered_map cbegin()+ number //常量复杂度?

动态阵列的摊余时间

散列如何具有 o(1) 搜索时间? [复制]

c#中的每周贷款支付/摊销时间表计算逻辑

平均情况和摊销分析之间的差异

为啥动态数组必须以几何方式增加其容量以获得 O(1) 摊销的 push_back 时间复杂度?