持续摊还的时间

Var*_*pta 396 algorithm complexity-theory big-o

在讨论算法的时间复杂度时,"恒定摊还时间"是什么意思?

Art*_*ius 748

摊销时间用简单的术语解释:

如果您做了一百万次操作,那么您并不真正关心该操作的最坏情况或最佳情况 - 您关心的是当您重复操作一百万次时需要花费多少时间.

因此,如果操作偶尔会非常缓慢并不重要,只要"偶尔"很少见,以便缓慢消除.基本上摊还的时间意味着"如果你做很多操作,每次操作需要的平均时间".摊销时间不必保持不变; 你可以有线性和对数的摊销时间或其他任何东西.

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

所以每次放大时,你花费的时间是最后一次放大的两倍.但是你在做这件事之前也等了两倍!因此,每次放大的成本可以在插入中"展开".这意味着从长远来看,将m个项目添加到数组所花费的总时间是O(m),因此摊销时间(即每次插入的时间)是O(1).

  • 只是表示符号:O(n)的摊销常量执行时间通常写为O(n)+,而不是O(n).添加加号表示执行时间不能保证为O(n),实际上可能超过执行时间. (56认同)
  • 我不同意“您真的不在乎最坏的情况”。这取决于用例。如果最后您只对所引用的100万次运算的结果感兴趣,那么您实际上并不在乎。但是,如果这是一个实时应用程序,需要不断读取数据然后对其做出响应,那么每处理一百万个数据项,处理该数据所花费的时间比正常时间长一百万倍,那么您可能会遇到一个大问题! (2认同)
  • @Jeffpowrs我认为[O(n)是线性时间,O(1)是常数时间](https://en.wikipedia.org/wiki/Time_complexity)。那么这是否意味着 O(1)+ 将摊销常数时间,而 O(n)+ 将摊销_线性_时间? (2认同)
  • @JohnMeyer 是的。 (2认同)

Mat*_*son 55

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

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

或者更严格地说,

有一个常数c,这样对于 长度为L的每个操作序列(也是以昂贵的操作结尾),时间不大于c*L(感谢RafałFowgird)

  • "经过足够大量的操作" - 不变的摊销时间不需要这种条件.存在常数c,使得对于长度为L的每个*操作序列(也以昂贵的操作结束),时间不大于c*L. (11认同)
  • @talekeDskobaDa 这不是一个任意的例子,而是一个广泛使用的算法。如果我们按照您的建议一次为一个条目分配空间,则插入单个值的摊销时间将为 O(n)。如果我们在空间满时将空间加倍,则摊余时间会好得多,O(1)。需要明确的是,一次为一项分配空间的问题是数组需要一大块连续空间。从操作系统获取更大的块很容易,但通常不可能扩展现有块,因为在它之后可能直接存储一些其他数据。 (2认同)

Meg*_*ozg 23

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

情节

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

  • @sqreept 因为该图上的纵坐标表示操作数,并且“push_back”无论如何都会添加到整体中,无论它是否恒定 (3认同)
  • 为什么分配之间存在斜率?难道不应该是水平的来描述所花费的恒定时间吗? (2认同)

Man*_*ddy 13

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

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

"动态阵列

动态阵列推送操作的摊销分析

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

通常,如果我们考虑将任意数量的n推送到大小为n的数组,我们注意到推送操作需要恒定的时间,除了最后一个需要O(n)时间来执行大小倍增操作.由于总共有n个操作,我们可以取平均值,并发现将元素推送到动态数组上需要:O(n/n)= O(1),恒定时间."

以我作为一个简单故事的理解:

假设你有很多钱.而且,你想把它们堆放在一个房间里.并且,你现在或将来需要很长的手和腿.而且,你必须在一个房间里填充所有,所以很容易锁定它.

所以,你走到房间的尽头,然后开始堆叠它们.当你堆叠它们时,房间会慢慢耗尽空间.但是,当你填充它很容易堆叠它们.得到钱,把钱.简单.这是O(1).我们不需要移动任何以前的钱.

一旦房间空间不足.我们需要另一个更大的房间.这里有一个问题,因为我们只有一个房间,所以我们只能有一个锁,我们需要把那个房间里现有的所有钱都搬进新的更大的房间.所以,把所有的钱从小房间搬到更大的房间.也就是说,再次堆叠所有这些.所以,我们需要移动所有以前的钱.所以,它是O(N).(假设N是以前货币的总金额)

换句话说,很容易直到N,只有1次操作,但是当我们需要搬到更大的房间时,我们做了N次操作.因此,换句话说,如果我们平均,则在开始时是1个插入,而在移动到另一个房间时再移动1个.总计2次操作,一次插入,一次移动.

假设即使在小房间中N大到100万,那么与N(100万)相比,2个操作实际上不是相当的数字,因此它被认为是常数或O(1).

假设我们在另一个更大的房间做了以上所有,并再次需要移动.它仍然是一样的.比方说,N2(比如10亿)是更大房间里新的金额

所以,我们有N2(其中包括之前的N,因为我们从小房间移动到更大的房间)

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

因此,即使对于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(最坏情况/操作数量)。我最喜欢这个答案。 (2认同)

lif*_*ful 6

我创建了这个简单的 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()
Run Code Online (Sandbox Code Playgroud)

这会产生以下情节在此输入图像描述

  • 每个附加行所花费的时间非常清楚 (3认同)