ArrayList的addType value方法O1如何摊销时间复杂度?

问题描述

大多数ArrayList的实现都在内部使用数组,并且在将元素添加到列表中时已经用尽了大小,它实际上通过执行以下操作来调整大小或“增长”:

  • 使用新一批新分配的内存来缓存新阵列。
  • 将内部数组的所有元素复制到新数组中。
  • 将内部数组设置为新数组。
  • 将内部数组的索引N - 1设置为元素对象,其中N是数组的新大小。

提供的解释是,对于平均加法操作而言,增加列表是非常必要的,因此平均加法的时间复杂度为O(1),因此摊销了固定时间。

我对此感到困惑。假设列表增加Q。简单的算术数列将向您展示,如果我将x元素添加到ArrayList中,则如果x^2 + Qx / 2Q的大小是{{的几倍],则内部完成的元素复制总数为x。 1}}。

当然,对于添加的前几个值,时间很可能是恒定的,但是对于添加的足够多的元素,我们看到对每个添加操作的平均时间复杂度线性或Q。因此,将大量元素添加到列表需要花费指数时间。我不明白单个加法操作的摊还时间复杂度如何恒定。有什么我想念的吗?

编辑:我没有意识到列表的增长实际上是几何的,这优化了摊销时间的复杂性。

结论:

动态列表的线性增长

O(n)

对于N = kQ个插入

副本:

N + 1

元素初始化:

  Q + 2Q + 3Q + … + kQ
= (k / 2)(2Q + (k - 1)Q)
= (k / 2)(Q + kQ) 
= (kQ + k^2 * Q) / 2 
-> kQ + k^2 * Q

廉价插入:

  Q + 2Q + 3Q + 4Q + … + (k + 1) * Q 
= ((k + 1) / 2)(2Q + kQ) 
= (k^2 * Q + 2kQ + 2Q + kQ) / 2 
-> k^2 * Q + 3kQ + 2Q

总费用: kQ + 1 -> kQ

每次插入的摊销成本:

2Q * k^2 + 5kQ + 2Q

动态列表的几何增长

2k + 5 + 2 / k -> 2k + 2 / k -> O(N / Q) -> O(N)

对于N = Q^k个插入

副本:

N + 1

元素初始化:

  1 + Q + Q^2 + … +  Q^k 
= (1 - Q^(k + 1)) / (1 - Q) 
-> Q^k

廉价插入:

  1 + Q + Q^2 + … + Q^(k + 1) 
= (1 - Q^(k + 2)) / (1 - Q) 
-> Q^(k + 1)

总费用: Q^k + 1 -> Q^k

每次插入的摊销成本:

2Q^k + Q^(k + 1)

比较

数组的几何大小调整/增长是恒定时间,而线性大小调整是线性时间。比较这两种增长方法以查看性能差异以及为什么选择ArrayLists进行几何增长很有趣。

解决方法

在不失一般性的前提下,假设列表的初始容量为1。我们进一步假定,每次插入超出容量时,容量都会加倍。现在考虑插入2^k + 1元素(这是最坏的情况,因为最后的操作会触发动态增长)。

k个插入可触发动态增长,其累积成本为

1 + 2 + 4 + 8 + ... + 2^k = 2^(k+1) - 1

其他“便宜”插入的累积费用为2^k - k + 1

但是我们对摊销复杂度感兴趣,因此我们必须对所有2^k + 1个操作取平均值:

  (2^(k+1) + 2^k - k) / (2^k + 1)
< (2^(k+1) + 2^k - k) / 2^k
= 2 + 1 - k/2^k
= O(1)

因此,将2^(k+1)元素插入列表具有摊销时间复杂度为O(1)每次插入,并且常数因子接近3。列表中的任何其他元素数量都不会更糟,因此每次插入的摊销时间复杂度通常为O(1)。