从 List<E> 中获取 n 个随机元素?

2022-08-31 11:48:54

如何从 ?理想情况下,我希望能够连续调用该方法以获取另一个x元素,而无需替换。ArrayList<E>take()


答案 1

主要有两种方式。

  1. 使用 Random#nextInt(int)

    List<Foo> list = createItSomehow();
    Random random = new Random();
    Foo foo = list.get(random.nextInt(list.size()));
    

    但是,不能保证连续调用返回唯一元素。n

  2. 使用 Collections#shuffle()

    List<Foo> list = createItSomehow();
    Collections.shuffle(list);
    Foo foo = list.get(0);
    

    它使您能够通过递增的索引获取唯一元素(假设列表本身包含唯一元素)。n


如果您想知道是否有Java 8流方法;不,没有内置的。在标准API中没有这样的东西(还没有?你可以尝试下面这样的东西,同时仍然满足严格的合同(尽管分布非常糟糕):Comparator#randomOrder()Comparator

List<Foo> list = createItSomehow();
int random = new Random().nextInt();
Foo foo = list.stream().sorted(Comparator.comparingInt(o -> System.identityHashCode(o) ^ random)).findFirst().get();

相反,最好使用。Collections#shuffle()


答案 2

到目前为止,大多数建议的解决方案都建议通过检查唯一性来进行完整的列表随机洗牌或连续随机选择,并在需要时重试。

但是,我们可以利用Durstenfeld算法(我们这个时代最流行的Fisher-Yates变体)。

Durstenfeld的解决方案是将“击中”的数字移动到列表的末尾,方法是在每次迭代时将它们与最后一个未击中的数字交换。

由于上述原因,我们不需要对整个列表进行随机排序,而是运行循环的步骤与返回所需的元素数一样多。该算法确保如果我们使用完美的随机函数,则列表末尾的最后 N 个元素是 100% 随机的。

在我们需要从数组/列表中选择预定(最大)数量的随机元素的许多现实世界场景中,这种优化的方法对于各种纸牌游戏非常有用,例如德州扑克,您先验地知道每个游戏要使用的纸牌数量;通常只需要从套牌中抽取有限数量的牌。

public static <E> List<E> pickNRandomElements(List<E> list, int n, Random r) {
    int length = list.size();

    if (length < n) return null;

    //We don't need to shuffle the whole list
    for (int i = length - 1; i >= length - n; --i)
    {
        Collections.swap(list, i , r.nextInt(i + 1));
    }
    return list.subList(length - n, length);
}

public static <E> List<E> pickNRandomElements(List<E> list, int n) {
    return pickNRandomElements(list, n, ThreadLocalRandom.current());
}