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

Posted

技术标签:

【中文标题】从 List<E> 中取 n 个随机元素?【英文标题】:Take n random elements from a List<E>? 【发布时间】:2011-06-09 18:39:51 【问题描述】:

如何从ArrayList&lt;E&gt; 中获取 n 个随机元素?理想情况下,我希望能够连续调用 take() 方法来获取另一个 x 元素,而无需替换。

【问题讨论】:

到目前为止你得到了什么?如果你得到另一个 x 元素,你能再次从前一组中挑选元素,还是必须一直都不同,直到所有元素都被挑选出来? (那么,接下来呢?) 无替换。当你一无所有时,你不应该得到任何回报。 【参考方案1】:

如果您想从列表中连续挑选 n 个元素,并且能够做到这一点而无需一遍又一遍地替换,那么您最好随机排列元素,然后以 n 个块为单位取出块。如果您随机排列列表,则可以保证您选择的每个块的统计随机性。也许最简单的方法是使用Collections.shuffle

【讨论】:

最简单的方法是调用 java.util.Collections.shuffle()【参考方案2】:

两种主要方式。

    使用Random#nextInt(int):

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

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

    使用Collections#shuffle():

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

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


如果您想知道是否有 Java 8 Stream 方法;不,没有内置的。标准 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()

【讨论】:

我有一个 4000 个单词的列表,每次按下刷新按钮时,我都必须从该列表中取出 5 个单词,我正在使用您答案的第二个选项。它在多大程度上保证我将始终获得唯一值,即概率是多少? @Prateek:如果您有任何问题,请按“”按钮。不要按“添加评论”或“发布答案”按钮。 我知道何时使用哪个按钮,我的评论与您已经发布的答案有些相关,所以我不想创建一个新的 if 线程并且正在寻找内联响应,无论如何谢谢。 请记住,Collections.shuffle() 使用 Fisher-Yates shuffle 算法的一个版本,其中包含 Random 的内部实例。 Random 类使用 long 作为其种子值,这意味着它最多只能为您提供 2^32 种可能的排列。这不足以以所有排列的均匀概率对超过 12 个元素进行混洗(即,某些排列永远不会出现)。如果您能胜任该任务,您将希望使用 Collections.shuffle(list,random) 代替,其中 random 是 SecureRandom 的实例或您自己的自定义 Random 扩展。 Matunos - 就其价值而言,java.util.Random 的有效种子大小为 2^48,但正如您所说,仍然值得牢记的是,您可能需要选择更好的生成器.我仍然会提倡我提到的简单地选择具有相关概率的项目的方法(你仍然需要与随机数相同数量的随机数,但你不必交换所有指针,可能更好的内存位置,并且有一旦您选择了所有必需的元素,就有机会“提前”终止循环)。【参考方案3】:

做到这一点的一种公平方法是遍历列表,在第 n 次迭代中计算是否选择第 n 个元素的概率,这本质上是您仍然需要选择的项目数量的分数列表其余部分中可用的元素数。例如:

public static <T> T[] pickSample(T[] population, int nSamplesNeeded, Random r) 
  T[] ret = (T[]) Array.newInstance(population.getClass().getComponentType(),
                                    nSamplesNeeded);
  int nPicked = 0, i = 0, nLeft = population.length;
  while (nSamplesNeeded > 0) 
    int rand = r.nextInt(nLeft);
    if (rand < nSamplesNeeded) 
      ret[nPicked++] = population[i];
      nSamplesNeeded--;
    
    nLeft--;
    i++;
  
  return ret;

(这段代码复制自我不久前在picking a random sample from a list 上写的一个页面。)

【讨论】:

bravo -- 这应该是最佳答案,因为它是最模块化和最便携的【参考方案4】:

简单明了

   // define ArrayList to hold Integer objects
    ArrayList<Integer> arrayList = new ArrayList<>();

    for (int i = 0; i < maxRange; i++) 
        arrayList.add(i + 1);
    

    // shuffle list
    Collections.shuffle(arrayList);

    // adding defined amount of numbers to target list
    ArrayList<Integer> targetList = new ArrayList<>();
    for (int j = 0; j < amount; j++) 
        targetList.add(arrayList.get(j)); 
    

    return targetList;

【讨论】:

我没有看到arrayListtargetList之间的相关性。 应该是 targetList.add(arrayList.get(j))【参考方案5】:

使用以下类:

import java.util.Enumeration;
import java.util.Random;

public class RandomPermuteIterator implements Enumeration<Long> 
    int c = 1013904223, a = 1664525;
    long seed, N, m, next;
    boolean hasNext = true;

    public RandomPermuteIterator(long N) throws Exception 
        if (N <= 0 || N > Math.pow(2, 62)) throw new Exception("Unsupported size: " + N);
        this.N = N;
        m = (long) Math.pow(2, Math.ceil(Math.log(N) / Math.log(2)));
        next = seed = new Random().nextInt((int) Math.min(N, Integer.MAX_VALUE));
    

    public static void main(String[] args) throws Exception 
        RandomPermuteIterator r = new RandomPermuteIterator(100);
        while (r.hasMoreElements()) System.out.print(r.nextElement() + " ");
    

    @Override
    public boolean hasMoreElements() 
        return hasNext;
    

    @Override
    public Long nextElement() 
        next = (a * next + c) % m;
        while (next >= N) next = (a * next + c) % m;
        if (next == seed) hasNext = false;
        return  next;
    

【讨论】:

【参考方案6】:

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

但是,我们可以利用 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());

【讨论】:

感谢您指出这一点。我有一种情况,我需要从一个大列表中删除少量元素,我确信洗牌整个列表不是最好的方法,但我对如何从一口气列出了一份清单。将它们交换到列表的末尾,然后将其截断,是一个优雅的解决方案。【参考方案7】:

继续选择一个随机元素,并确保不再选择相同的元素:

public static <E> List<E> selectRandomElements(List<E> list, int amount)

    // Avoid a deadlock
    if (amount >= list.size())
    
        return list;
    

    List<E> selected = new ArrayList<>();
    Random random = new Random();
    int listSize = list.size();

    // Get a random item until we got the requested amount
    while (selected.size() < amount)
    
        int randomIndex = random.nextInt(listSize);
        E element = list.get(randomIndex);

        if (!selected.contains(element))
        
            selected.add(element);
        
    

    return selected;

理论上这可以无休止地运行,但实际上它很好。越接近整个原始列表,它的运行时间就越慢,但这不是选择随机子列表的重点,是吗?

【讨论】:

【参考方案8】:

以下类从任何类型的列表中检索 N 项。如果您提供种子,那么在每次运行时它将返回相同的列表,否则,新列表的项目将在每次运行时更改。您可以在运行主要方法时检查其行为。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;

public class NRandomItem<T> 
    private final List<T> initialList;

    public NRandomItem(List<T> list) 
        this.initialList = list;
    

    /**
     * Do not provide seed, if you want different items on each run.
     * 
     * @param numberOfItem
     * @return
     */
    public List<T> retrieve(int numberOfItem) 
        int seed = new Random().nextInt();
        return retrieve(seed, numberOfItem);
    

    /**
     * The same seed will always return the same random list.
     * 
     * @param seed,
     *            the seed of random item generator.
     * @param numberOfItem,
     *            the number of items to be retrieved from the list
     * @return the list of random items
     */
    public List<T> retrieve(int seed, int numberOfItem) 
        Random rand = new Random(seed);

        Collections.shuffle(initialList, rand);
        // Create new list with the number of item size
        List<T> newList = new ArrayList<>();
        for (int i = 0; i < numberOfItem; i++) 
            newList.add(initialList.get(i));
        
        return newList;
    

    public static void main(String[] args) 
        List<String> l1 = Arrays.asList("Foo", "Bar", "Baz", "Qux");
        int seedValue = 10;
        NRandomItem<String> r1 = new NRandomItem<>(l1);

        System.out.println(String.format("%s", r1.retrieve(seedValue, 2)));
    

【讨论】:

【参考方案9】:

正如其他答案中所述,Collections.shuffle 在源列表很大时效率不是很高,因为复制。这是一个 Java 8 单行代码:

如果您不需要来自源的许多元素,则在 ArrayList 等随机访问列表上足够高效 不修改源代码 不保证唯一性,如果它对您来说不是超级重要的话。如果您从 100 个中选择 5 个,那么这些元素很有可能是独一无二的。

代码:

private static <E> List<E> pickRandom(List<E> list, int n) 
  return new Random().ints(n, 0, list.size()).mapToObj(list::get).collect(Collectors.toList());

但是,对于没有快速随机访问的列表(如 LinkedList),复杂度将是n*O(list_size)

【讨论】:

【参考方案10】:

此解决方案不会修改原始列表或以其他方式随着列表大小而增加复杂性。

要从 7 个列表中获取 4 个样本,我们只需从所有 7 个元素中选择一个随机元素,然后从其余 6 个中选择一个随机元素,依此类推。如果我们已经选择了索引 4、0、3,接下来我们从 0、1、2、3 中生成一个随机数,分别代表索引 1、2、5、6。

static Random rand = new Random();

static <T> List<T> randomSample(List<T> list, int size) 
    List<T> sample = new ArrayList<>();

    for (int sortedSampleIndices[] = new int[size], i = 0; i < size; i++) 
        int index = rand.nextInt(list.size() - i);

        int j = 0;
        for (; j < i && index >= sortedSampleIndices[j]; j++)
            index++;
        sample.add(list.get(index));

        for (; j <= i; j++) 
            int temp = sortedSampleIndices[j];
            sortedSampleIndices[j] = index;
            index = temp;
        
    

    return sample;

【讨论】:

【参考方案11】:

所有这些答案都需要一个可修改的列表或遇到性能问题

这是一个 swift sn-p,它需要 O(k) 额外空间,并且保证在 O(k) 时间内运行,并且不需要可修改的数组。 (在地图中执行洗牌)

  func getRandomElementsFrom(array: [Int], count: Int = 8) -> [Int] 
    if array.count <= count 
        return array
    

    var mapper = [Int: Int]()
    var results = [Int]()

    for i in 0..<count 
        let randomIndex = Int.random(in: 0..<array.count - i)

        if let existing = mapper[randomIndex] 
            results.append(array[existing])
         else 
            let element = array[randomIndex]
            results.append(element)
        

        let targetIndex = array.count - 1 - i
        mapper[randomIndex] = mapper[targetIndex] ?? targetIndex 
    

    return results

【讨论】:

【参考方案12】:

以下方法返回从参数列表列表中获取的新 Min(n, list.size()) 随机元素列表。请记住,每次通话后都会修改列表列表。因此,每次调用都会“消耗”原始列表并从中返回 n 个随机元素:

public static <T> List<T> nextRandomN(List<T> list, int n) 
  return Stream
    .generate(() -> list.remove((int) (list.size() * Math.random())))
    .limit(Math.min(list.size(), n))
    .collect(Collectors.toList());

示例用法:

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));

System.out.println(nextRandomN(list, 3).toString());
System.out.println(nextRandomN(list, 3).toString());
System.out.println(nextRandomN(list, 3).toString());
System.out.println(nextRandomN(list, 3).toString());

示例输出:

[8, 2, 3]
[4, 10, 7]
[1, 5, 9]
[6]

【讨论】:

看起来每次调用 nextRandomN() 都会从原始列表中删除元素。对 nextRandomN() 的任何其他调用都将返回一个空列表。

以上是关于从 List<E> 中取 n 个随机元素?的主要内容,如果未能解决你的问题,请参考以下文章

无重复元素的组合算法/n个列表中取n个不同的数

从 C# 中的 List<T> 中选择 N 个随机元素

从 C# 中的 List<T> 中选择 N 个随机元素的算法[重复]

给出两个整数n和k,返回从1到n中取k个数字的所有可能的组合

java 实现每次从list中取5000条数据放入新list

如何证明,在n个元素组成的集合中取n+1个不同的三元子集,求证必有两个子集,它们恰有一个公共元?