调整阵列大小时我应该添加多少?

时间:2009-12-02 02:09:13

标签: java arrays optimization

我正在与另一名学生竞赛,以制作最快的家庭作业版本,并且由于性能原因我没有使用ArrayList(我自己将基准时间从56秒调整为4),但是我想知道每次需要时我应该调整数组的大小。具体来说,我的代码的相关部分是这样的:

private Node[] list;
private int size; // The number of items in the list
private static final int N; // How much to resize the list by every time

public MyClass(){
  list = new Node[N];
}

public void add(Node newNode){
  if(size == list.length){
    list = Arrays.copyOf(list, size + N);
  }
  list[size] = newNode;
  size++;
}

TL; DR:我该怎么做N

9 个答案:

答案 0 :(得分:6)

建议在调整大小时将数组的大小加倍。将尺寸加倍会导致线性时间成本摊销。

天真的想法是,调整大小值有两个成本:

  • 复制性能成本 - 将元素从先前阵列复制到新阵列的成本,以及
  • 内存开销成本 - 未使用的分配内存的成本。

如果要通过一次添加一个元素来重新调整数组大小,则内存开销为零,但复制成本变为二次方。如果要分配太多的插槽,复制成本将是线性的,但内存开销太大。

加倍导致线性摊销成本(即很长一段时间,复制成本与阵列大小成线性关系),并且保证不会浪费超过阵列的一半。

更新:顺便说一句,显然Java的ArrayList扩展了(3/2)。这使得内存保守一点,但在复制方面要花费更多。使用基准测试不会受到影响。

MINOR Correction:加倍会使成本调整大小线性摊销,但会确保您有一个摊销的常数时间插入。检查CMU's lecture on Amortized Analysis

答案 1 :(得分:5)

3/2很可能被选为“干净利落但小于phi”的东西。 2003年11月回归an epic thread on comp.lang.c++.moderated,探讨 phi 如何在重新分配首次匹配的分配器时重新使用先前分配的存储。

首次提及 phi 对此问题的应用,请参阅post #7 from Andrew Koenig

答案 2 :(得分:2)

如果你大致知道将要有多少项,那么将数组或ArrayList预分配到该大小,你将永远不必扩展。无与伦比的表现!

如果做不到这一点,实现良好摊销成本的合理方法是保持一定比例的抗冻性。 100%或50%是常见的。

答案 3 :(得分:2)

您应该将列表的大小调整为以前大小的倍数,而不是每次都添加常量。

例如:

newSize = oldSize * 2;

newSize = oldSize + N;

答案 4 :(得分:2)

每次需要调整大小时,大小加倍,除非您知道更多或更少是最好的。

如果内存不是问题,那么首先从一个大数组开始。

答案 5 :(得分:2)

您的代码似乎与ArrayList完全相同 - 如果您知道将使用大型列表,则可以在创建列表时将其传递给初始大小,并且完全避免调整大小。这个过程假设您需要原始速度和内存消耗不是问题。

答案 6 :(得分:1)

根据其中一个答案的评论:

  

问题在于记忆不是   问题,但我正在任意阅读   大文件。

试试这个:

new ArrayList<Node>((int)file.length());

您也可以使用您的阵列。然后在任何一种情况下都不应该调整大小,因为数组将是文件的大小(假设文件不长于int ...)。

答案 7 :(得分:0)

为了获得最佳性能,您需要尽可能少地调整大小。将初始大小设置为您通常需要的大小,而不是从N个元素开始。在这种情况下,您为N选择的值将更少。


如果您要创建大量不同大小的列表对象,那么您将需要使用基于池的分配器,而不是在退出之前释放内存。

要完全消除复制操作,可以使用数组列表

答案 8 :(得分:0)

这里有一个类比,很久很久以前,当我以前在大型机上工作时,我们使用了一个名为VSAM的文件系统,它要求你指定初始文件大小和所需的可用空间量。

每当自由空间的数量降到所需的阈值以下时,在程序继续处理时,所需的可用空间量将在后台分配。

看看是否可以在java中使用单独的线程来分配额外的空间并在主线程继续处理时将其“附加”到数组的末尾,这将会很有趣。