Java ArrayList加法(类型值)方法(1)摊销时间复杂度如何?

Java ArrayList加法(类型值)方法(1)摊销时间复杂度如何?,java,arraylist,data-structures,time-complexity,Java,Arraylist,Data Structures,Time Complexity,ArrayList的大多数实现在内部使用数组,当向列表中添加元素时,数组大小已经耗尽,它会通过执行以下操作来调整大小或“增长”: 使用新分配的新内存批缓存新阵列 将内部数组的所有元素复制到新数组中 将内部阵列设置为新阵列 将内部数组的索引N-1设置为元素对象,其中N是数组的新大小 提供的解释是,增加列表对于平均添加操作来说是一种罕见的必要性,因此平均添加的时间复杂度为O(1),因此摊销固定时间 我不明白这有什么意义。假设列表按Q增长。简单算术系列将向您展示,如果我向ArrayList添加x元

ArrayList的大多数实现在内部使用数组,当向列表中添加元素时,数组大小已经耗尽,它会通过执行以下操作来调整大小或“增长”:

  • 使用新分配的新内存批缓存新阵列
  • 将内部数组的所有元素复制到新数组中
  • 将内部阵列设置为新阵列
  • 将内部数组的索引
    N-1
    设置为元素对象,其中
    N
    是数组的新大小
提供的解释是,增加列表对于平均添加操作来说是一种罕见的必要性,因此平均添加的时间复杂度为
O(1)
,因此摊销固定时间

我不明白这有什么意义。假设列表按
Q
增长。简单算术系列将向您展示,如果我向ArrayList添加
x
元素,内部完成的元素副本总数是
x^2+Qx/2Q
,如果
x
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
  1 + Q + Q^2 + … +  Q^k 
= (1 - Q^(k + 1)) / (1 - Q) 
-> Q^k
元素初始化:

  Q + 2Q + 3Q + 4Q + … + (k + 1) * Q 
= ((k + 1) / 2)(2Q + kQ) 
= (k^2 * Q + 2kQ + 2Q + kQ) / 2 
-> k^2 * Q + 3kQ + 2Q
  1 + Q + Q^2 + … + Q^(k + 1) 
= (1 - Q^(k + 2)) / (1 - Q) 
-> Q^(k + 1)
廉价插入:

  kQ + 1 
-> kQ
  Q^k + 1 
-> Q^k
总成本:
2Q*k^2+5kQ+2Q

每次插入的摊余成本:

  2k + 5 + 2 / k 
-> 2k + 2 / k
-> O(N / Q)
-> O(N)
  2 + Q
-> O(1)
动态列表的几何增长

N=Q^k

对于
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
  1 + Q + Q^2 + … +  Q^k 
= (1 - Q^(k + 1)) / (1 - Q) 
-> Q^k
元素初始化:

  Q + 2Q + 3Q + 4Q + … + (k + 1) * Q 
= ((k + 1) / 2)(2Q + kQ) 
= (k^2 * Q + 2kQ + 2Q + kQ) / 2 
-> k^2 * Q + 3kQ + 2Q
  1 + Q + Q^2 + … + Q^(k + 1) 
= (1 - Q^(k + 2)) / (1 - Q) 
-> Q^(k + 1)
廉价插入:

  kQ + 1 
-> kQ
  Q^k + 1 
-> Q^k
总成本:
2Q^k+Q^(k+1)

每次插入的摊余成本:

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


阵列的几何尺寸调整/增长是恒定时间,而线性尺寸调整是线性时间。比较这两种增长方法以了解性能差异以及为什么选择ArrayList以几何方式增长是很有趣的。

在不丧失通用性的情况下,假设列表的初始容量为1。我们进一步假设,每次插入超过容量时,容量都会加倍。现在考虑插入<代码> 2 ^ k+1 < /COD>元素(这是最坏的情况,因为最后一个操作触发动态增长)。 有
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)+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)。

在不丧失一般性的情况下,假设列表的初始容量为1。我们进一步假设,每次插入超过容量时,容量都会加倍。现在考虑插入<代码> 2 ^ k+1 < /COD>元素(这是最坏的情况,因为最后一个操作触发动态增长)。 有
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)+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)。

您假设列表按
Q
增长以添加
Q
元素。事实并非如此。列表按
x
增长,其中
x
是列表当前大小的倍数。@ElliottFrisch问题不会仍然存在吗?在某个时刻,当数组耗尽时,您仍然需要复制所有
N
元素。将不断增长的大小设置为列表当前大小的倍数只会推迟以后的工作。我可以说,对于添加到列表中的足够多的元素,平均时间复杂度是
O(n)
至少完成了n/2个元素总副本。更正:至少
n/k
元素总副本,其中
k
是列表增长的倍数。您假设列表按
Q
增长以添加
Q
元素。事实并非如此。列表按
x
增长,其中
x
是列表当前大小的倍数。@ElliottFrisch问题不会仍然存在吗?在某个时刻,当数组耗尽时,您仍然需要复制所有
N
元素。将不断增长的大小设置为列表当前大小的倍数只会推迟以后的工作。我可以说,对于足够多的