【问题标题】:Amortized analysis of std::vector insertionstd::vector 插入的摊销分析
【发布时间】:2011-09-26 21:20:04
【问题描述】:

我们如何分析 std::vector 后面的插入(push_back)?每次插入的摊销时间是 O(1)。特别是在video in channel9 by Stephan T Lavavejin this ( 17:42 onwards ) 中,他说为了获得最佳性能,微软对这种方法的实现将向量的容量提高了大约 1.5。

这个常数是如何确定的?

【问题讨论】:

  • 你确定你的意思是插入吗?我认为只有插入在末尾​​i>,或push_back,是摊销O(1);任意插入与需要移动的元素数量成线性关系。
  • 哦,我对此表示怀疑,感谢您提及...将编辑它
  • 为什么人们投票结束时认为“离题”和“没有建设性”?投票结束“重复”可能是可以理解的,但不是给出的原因。潜在选民:当您不理解某个问题时,请不要投票。

标签: c++ algorithm stl stdvector amortized-analysis


【解决方案1】:

假设您的意思是 push_back 而不是插入,我相信重要的部分是乘以某个常数(而不是每次抓取 N 个更多元素),只要您这样做,您就会得到摊销的常数时间.更改因子会更改平均情况和最差情况的性能。

具体来说: 如果您的常数因子太大,您将获得良好的平均情况性能,但最坏情况下的性能较差,尤其是当阵列变大时。例如,假设仅仅因为您推送了第 10001 个元素,就将 10000 大小的向量加倍 (2x)。编辑:正如迈克尔伯尔间接指出的那样,这里的真正成本可能是你的记忆力增长得比你需要的要大得多。我要补充一点,如果您的因素太大,缓存问题会影响速度。可以说,如果您的增长超出您的需要,就会产生实际成本(内存和计算)。

但是,如果您的常数因子太小,例如 (1.1x),那么您将获得良好的最坏情况性能,但平均性能较差,因为您将不得不承担重新分配太多次的成本.

Also, see Jon Skeet's answer to a similar question previously.(感谢@Bo Persson)

关于分析的更多信息:假设您有 n 要推后的项目和 M 的乘法因子。那么重新分配的数量将大致是Mn (log_M(n)) 的日志基数。而ith 重新分配的成本将与M^i 成正比(Mith 的幂)。那么所有推回的总时间将是M^1 + M^2 + ... M^(log_M(n))。推回的数量是n,因此你得到这个系列(这是一个几何系列,在极限范围内减少到大约(nM)/(M-1))除以n。这大致是一个常数,M/(M-1)

对于较大的 M 值,您将超出很多并且分配的数量远远超出您合理经常需要的数量(我在上面提到过)。对于较小的 M 值(接近 1),此常量 M/(M-1) 会变大。这个因素直接影响平均时间。

【讨论】:

  • 为什么将 10000 个元素向量的分配加倍比分配一个包含其他数量(大于 10000)元素的新块更糟糕?
  • 所以你是说因子太大的真正问题是你会占用太多内存?还是我错过了重点?没错,真正的成本可能是重新分配后发生的复制。
  • 在内存消耗和恒定复杂性因素之间进行了一些权衡。
  • 我根本不明白这是如何回答这个问题的。但是 OP 已经表示确实如此,所以我没有反对。只是在说'。 :-)
  • @Chris A:实际上,优化(内存方面)包括在几个大小间隔内使用不同的常数。你一开始就快速成长,并在变大时减少这个因素。只要这套因素始终保持> 1(甚至是它的极限),那么复杂性仍然得到保证。我喜欢自适应算法
【解决方案2】:

您可以进行数学运算来尝试弄清楚这种事情是如何工作的。

使用渐近分析的一种流行方法是银行家方法。您所做的是用额外的成本标记所有操作,“保存”它以供以后支付昂贵的操作。


让我们做一些转储假设来简化数学:

  • 写入数组会花费1。 (在数组之间插入和移动也是如此)
  • 分配更大的数组是免费的。

我们的算法看起来像:

function insert(x){
    if n_elements >= maximum array size:
         move all elements to a new array that
         is K times larger than the current size
    add x to array
    n_elements += 1

显然,“最坏情况”发生在我们必须将元素移动到新数组时。让我们尝试通过将d 的常量标记添加到插入成本中来摊销,使每次操作的总成本达到(1 + d)

在调整数组大小后,我们有 (1/K) 的数组被填满,但没有节省任何资金。 当我们填满数组时,我们可以确保至少保存了d * (1 - 1/K) * N。由于这笔钱必须能够支付所有被移动的 N 个元素,我们可以找出 Kd 之间的关系:

d*(1 - 1/K)*N = N
d*(K-1)/K = 1
d = K/(K-1)

一张有用的表格:

k    d     1+d(total insertion cost)
1.0  inf   inf
1.1  11.0  12.0
1.5  3.0   4.0
2.0  2.0   3.0
3.0  1.5   2.5
4.0  1.3   2.3
inf  1.0   2.0

因此,您可以从中大致了解数学家对时间/内存权衡如何解决此问题的想法。当然,有一些注意事项:当数组元素减少时,我没有过度缩小数组,这只涵盖了最坏的情况,即没有元素被删除并且没有考虑分配额外内存的时间成本。

他们很可能进行了一系列实验性测试,最终弄清楚了这一点,但我写的大部分内容都无关紧要。

【讨论】:

    【解决方案3】:

    嗯,当你熟悉数字系统时,分析真的很简单,比如我们常用的十进制。

    为了简单起见,假设每次达到当前容量时,都会分配一个新的 10 倍大的缓冲区。

    如果原始缓冲区大小为 1,则第一次重新分配复制 1 个元素,第二个(现在缓冲区大小为 10)复制 10 个元素,依此类推。因此,如果进行五次重新分配,则执行 1+10+100+1000+10000 = 11111 个元素副本。乘以 9,得到 99999;现在加 1,你有 100000 = 10^5。或者换句话说,向后执行,为支持这 5 次重新分配而执行的元素副本数为 (10^5-1)/9。

    5 次重新分配,5 次乘以 10 后的缓冲区大小为 10^5。这大约是元素复制操作数的 9 倍。这意味着复制所花费的时间与生成的缓冲区大小大致呈线性关系。

    以 2 为底而不是 10 得到 (2^5-1)/1 = 2^5-1。

    其他碱基(或增加缓冲区大小的因素)等等。

    干杯&hth。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-11-08
      • 2016-03-16
      • 2012-12-09
      • 2019-08-10
      • 2015-05-20
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多