问题描述
大多数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)。