每次从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
个元素。
例如,您可以构造一个只设置了除 P
元素之外所有元素的 BitSet
:
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);
}
如果您只是从一个700万元素列表中删除零元素,则仍然需要将所有6999999个元素向右移动;但是任何其他的删除都不需要再进行额外的移动。这个算法的时间复杂度为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
的值;它还保持了原始顺序。
如果你想从一个有 N
个项目的列表中随机选出 K
个项目而不重复,有 choose(N, K) = N! / (K! * (N-K)!)
种方法可以实现。如果你想要以相等的概率选择所有元素,则应该选择这些 c(n,k)
种不同的配置之一。
当还剩下 k
个项目可供选择时,你可以:
- 选择第一个项目;然后从剩余的
n-1
个项目中选择 k-1
个项目;或者
- 不选择第一个项目;然后从剩余的
n-1
个项目中选择 k
个项目。
为了确保整体上选择这 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
因此,当您需要从n中取k项时,您应该在时间的k/n中首先取第一项。
需要指出的两种有趣情况是:
当k == n时,k/n = 1,因此您始终会取该元素。直观地看,如果您需要从n个项目中选出n个项目,则必须全部取出。
当k == 0时,k/n = 0,因此您永远不会取该元素。 直观地看,如果您已经选择了所有K个项目,则无需再选择任何项目。
要实现这一点,您可以在范围[0..n)内简单生成一个均匀分布的随机数r,并在r < k时从列表中“取”元素。
根据上面的实现,k = P - dst,并且n = list.size() - src。