问题 删除Java ArrayList中的对象 - 时间消耗


我正在尝试从大小为7,140,​​000的ArrayList中删除140,000个对象。我预计这将花费几秒钟(如果那样),而Java每千个对象需要多秒。这是我的代码:

     for (int i = list.size(); i > P; i--)
     {
         int size = list.size();

         int index = (int) (Math.random() * size);

         list.remove(index);
     }

注意:P是我之前设定为7,000,000的常数。

循环的目标是从列表中随机删除对象,直到其大小为7,000,000。

Java花了这么长时间,因为我开始使用超过700万个对象?我从来没有注意到过去从ArrayLists中删除这个效率问题。如果有帮助,我使用DrJava Beta IDE。


3408
2017-09-26 06:51


起源

剩余物品的顺序是否重要? - Piro
如果唯一的标准是制作7,000,000大小的列表,为什么需要随机进行并支付移动阵列的成本?你为什么不能删除7,140,​​000到7,000,000的元素? - Nishit


答案:


ArrayList由数组支持,因此修改需要将项目放在一边,在某些情况下甚至可以创建一个全新的数组。

一些可能的方案:

  1. 请考虑使用LinkedList或skip-list实现。请注意,在这里,删除一个项目仍然需要O(N)(或跳过列表中的O(logN)),因为它必须找到它。但是,您可以根据已删除的项目数来移动项目。

  2. 您可以随意将输入中的项目添加到新的ArrayList中,直到获得所需的项目数。您必须知道您添加了哪些项目,因此以线性方式遍历,并让随机选择器根据您移动的项目有多少步骤。

  3. 最简单的解决方案:随机播放整个输入数组,然后选择前M个项目。

这是解决方案#3的可能代码:

public static List<String> pickNRandom(List<String> lst, int m) {
    Collections.shuffle(lst);
    return lst.subList(0, n);
}

这里的缺点是它破坏了物品的顺序。您可以通过创建列表的副本作为输入来克服此问题,但这将占用更多内存(暂时)...


6
2017-09-26 07:17



好主意......我从未想过解决方案#3。十分优雅。是否有一个内置的Java函数我可以用来洗牌ArrayList?或者我必须编写自己的函数。另外,无法改变ArrayList中的所有元素导致我遇到的同样问题(许多元素不断被“向下移动”)? - Inertial Ignorance
@InertialIgnorance实际上有一个功能: docs.oracle.com/javase/6/docs/api/java/util/... ,但我从来没用过它。它很容易实现。您可以在列表的每个项目上使用循环,并与列表中的随机项目交换。解决方案3是我认为最好的,因为您可以使用结果项目最终创建新列表。唯一可能的问题是你破坏了输入项的顺序。您可以暂时保存订单,但这需要额外的空间...... - android developer
以下是我发现的与解决方案#3类似的示例答案: stackoverflow.com/a/8378788/878126 - android developer
以下是其他解决方案,类似于解决方案#1,#2我写过: en.wikipedia.org/wiki/Reservoir_sampling - android developer
说得通。在列表中运行一个循环并在每次迭代时交换两个随机对象应该非常快。在我的情况下,订单根本不重要,所以解决方案是理想的。 - Inertial Ignorance


每次从ArrayList中删除一个元素时,它都必须将具有较大索引的所有元素向下移动一个插槽。假设您删除了7M元素列表的第一个元素 - 那么您也必须移动6,999,999个元素。

如果你在循环中这样做,它将需要 O(n^2) 时间,在哪里 n 是列表的大小。对于7M元素列表,这将是非常缓慢的。

相反,如果您知道要提前删除哪些元素,则可以在一次传递中将所有元素向下移动:

int dst = 0;
for (int src = 0; src < list.size(); ++src) {
  if (!toRemove(src)) {
    list.set(dst++, list.get(src));
  }
}
list.subList(dst, list.size()).clear();

哪里 toRemove(src) 是一些功能,说明你是否要删除 src第 - 个元素。

例如,您可以构造一个 BitSet 除了 P 元素集:

BitSet toRemove = new BitSet(list.size());
for (int i = list.size(); i > P; i--) {
  int rand;
  do {
    rand = Math.random() * list.size();
  } while (toRemove.get(rand));
  toRemove.set(rand, true);
}

如果你只是从7M元素列表中删除零元素,你仍然必须将所有6,999,999个元素移到右边;但是任何其他删除都不需要在顶部进行任何更换。这个算法是 O(n),其中n是列表的大小。


编辑:你可以选择 P 列表中的元素(其中 P <= list.size()) 喜欢这个:

int dst = 0;
Random rand = new Random();
for (int src = 0; dst < P; ++src) {
  if (rand.nextInt(list.size() - src) < (P-dst)) {
    list.set(dst++, list.get(src));
  }
}
list.subList(dst, list.size()).clear();

此策略将以相同的概率(*)从列表中选择元素,并且适用于任何值 P;它还保留了原始订单。


如果你想要样品 K列表中的项目 N 没有绘制相同元素两次的项目,有 choose(N, K) = N! / (K! * (N-K)!) 这样做的方法。如果你想以相同的概率从列表中选择所有元素,那么你应该选择其中任何一个 c(n,k) 不同的配置。

什么时候有 k 留下来挑选的物品 n 项目,您将:

  • 选择第一项;然后选择 k-1 其余的项目 n-1 项目;要么
  • 不要选择第一项;然后选择 k 其余的项目 n-1 项目。

为了确保拣选的概率相等 K 总体而言,您需要根据从中挑选的组合数量选择两个选项中的一个 n-1 内容:

                                   #(combinations after taking first item) 
P(take first item) = ------------------------------------------------------------------
                     #(combinations after taking) + #(combinations after not taking)

                   = C(n-1,k-1) / (C(n-1, k-1) + C(n-1, k))

                   = ... working omitted ...

                   = k / n

所以,当你有了 k 剩下的物品 n,你应该采取第一项 k/n 的时间。

需要指出的两个有趣案例是:

  • 什么时候 k == nk/n = 1, 那么你 总是 拿元素。直观地说,如果你必须选择 n 物品 n,你必须把它们全部拿走。
  • 什么时候 k == 0k/n = 0, 那么你 决不 拿元素。直观地说,如果你已经选择了全部 K 你的物品,你不需要再拿走了。

要实现这一点,您可以简单地生成均匀分布的随机数 r 在范围中 [0..n),并从列表中“获取”元素 r < k

就上述实施而言, k = P - dst,和 n = list.size() - src


7
2017-09-26 07:02



我同意。 ArrayList 是一个数组 事实上的。只有在不打算删除它时才必须使用它,只需添加元素。如果您想从中删除元素,最好创建 另一个  ArrayList 并在那里只添加必需的元素。作为替代方案,您可以使用 LinkedList,当您想在中间修改列表(添加或删除其中的元素)时更有用。 - oleg.cherednik
感谢您的回答。我刚才做了一些自己的研究,它与你答案中的想法相符。我没有意识到每次传球都需要多少时间来移动所有元素,但这是有意义的。我的解决方案涉及将140,000个元素随机设置为null,然后将所有“非null”元素放入临时ArrayList中。然后我将列表设置为临时ArrayList。我认为,与您的解决方案相同,它按预期快速运行。 - Inertial Ignorance
@ oleg.cherednik ArrayList可以在一般使用中使用。如果它很小或者几乎不需要任何操作。这里太多了...... - android developer
@ oleg.cherednik是的,我最后做了你提到的事情。它工作正常。 - Inertial Ignorance
@InertialIgnorance我已经写了一些其他可能的解决方案,如果你愿意的话。 - android developer