我正在尝试从大小为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。
答案 0 :(得分:7)
每次从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 == n
,k/n = 1
时,您总是获取该元素。直观地说,如果你必须从n
中选择n
个项目,你必须全部拿走它们。k == 0
,k/n = 0
时,永远不会获取该元素。直觉上,如果您已经选择了所有K
件物品,则无需再接受任何物品。要实现此功能,您只需在r
范围内生成均匀分布的随机数[0..n)
,并在r < k
中“获取”列表中的元素。
就上述实施而言,k = P - dst
和n = list.size() - src
。
答案 1 :(得分:6)
ArrayList由数组支持,因此修改需要将项目放在一边,在某些情况下甚至可以创建一个全新的数组。
一些可能的解决方案:
请考虑使用LinkedList或skip-list实现。请注意,在这里,删除一个项目仍然需要O(N)(或跳过列表中的O(logN)),因为它必须找到它。但是,您可以根据已删除的项目数来遍历项目。
您可以随意将输入中的项目添加到新的ArrayList中,直到获得所需的项目数。您必须知道您添加了哪些项目,因此以线性方式遍历,并让随机选择器根据您移动的项目有多少步骤。
最简单的解决方案:随机播放整个输入数组,然后选择前M个项目。
以下是解决方案#3的可能代码:
public static List<String> pickNRandom(List<String> lst, int m) {
Collections.shuffle(lst);
return lst.subList(0, n);
}
这里的缺点是它破坏了物品的顺序。您可以通过创建列表的副本作为输入来克服此问题,但这会占用更多内存(暂时)......