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


77

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


到目前为止你得到了什么?如果再得到一个x元素,是否可以再次从先前的集合中选择元素,或者在选择所有元素之前必须一直都是不同的?(然后,接下来呢?)
Yanick Rochon

无需更换。当您没有剩余的东西时,您应该一无所有。

Answers:


109

两种主要方式。

  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 Stream方法;不,没有内置的。没有Comparator#randomOrder()标准API中的功能(还可以吗?)。您可以在满足严格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个单词。它能确保我始终获得唯一值的可能性有多大?即几率是多少?
2013年

1
@Prateek:如果您有问题,请按“问问题”按钮。不要按“添加评论”或“发布答案”按钮。
BalusC

3
我知道何时使用哪个按钮,我的评论在某种程度上与您已经发布的答案有关,所以我不想创建if的新线程并且正在寻找内联响应,无论如何要感谢。
2013年

8
请记住,Collections.shuffle()使用带有内部Random实例的Fisher-Yates随机算法版本。Random类的种子值使用long,这意味着它最多只能提供2 ^ 32个可能的排列。这不足以以所有排列的统一概率对不超过12个元素进行混排(也就是说,永远不会出现某些排列)。您将要使用Collections.shuffle(list,random)代替,其中random是SecureRandom的实例,或者是您自己的自定义Random扩展的实例(如果您要完成此任务)。
Matunos

Matunos-就其价值而言,java.util.Random的有效种子大小为2 ^ 48,但是正如您所说的那样,仍然需要记住,您可能需要选择一个更好的生成器。我仍然会提倡我提到的以相关概率简单地选择项目的方法(您仍然需要与随机播放相同数量的随机数,但是不必交换所有指针,可能会更好的内存位置,并且有一旦选择了所有必需的元素,就有机会“尽早”终止循环)。
尼尔·科菲

33

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

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

1
感谢您指出了这一点。我遇到的情况是我需要从大列表中删除少量元素,并且我确定对整个列表进行混洗并不是实现此目的的最佳方法,但是我一直迷恋于如何从中删除多个元素一举多得。将它们交换到列表的末尾,然后将其截断是一个很好的解决方案。
马特

10

如果您要从列表中连续选择n个元素,并且能够一遍又一遍地替换而不需要替换,则最好是随机排列这些元素,然后以n个块的形式取出块。如果您随机排列列表,则可以保证您选择的每个块的统计随机性。也许最简单的方法就是使用Collections.shuffle


3
最简单的方法是调用java.util.Collections.shuffle()
biziclop 2011年

7

简单明了

   // 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
David

应该是targetList.add(arrayList.get(j))
游牧

6

这样做的一种公平方法是遍历列表,在第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;
}

(这段代码是从我前一段时间写的从列表中随机抽取样本的页面复制的。)


太棒了-这应该是答案,因为它是最模块化和最便携的
Drew O'Meara

2

使用以下类:

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;
    }
}

2

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

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;
}

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


2

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

  • 如果您不需要源中的许多元素,那么在诸如ArrayList之类的随机访问列表上就足够高效了
  • 不修改源
  • 如果对您不重要,则不保证唯一性。如果您从一百个中选出五个,那么这些元素很有可能是唯一的。

码:

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)


0

下面的类从任何类型的列表中检索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)));
    }
}

0

此解决方案不会修改原始列表,也不会随着列表大小而增加复杂性。

要从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;
}

0

所有这些答案都需要可修改的列表,否则会产生性能问题

这是一个快速的代码段,它需要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
}

0

下面的方法返回一个新的Min(n,list.size())随机元素列表,该列表取自paramenter List列表。请记住,每次调用后都会修改列表列表。因此,每个调用都将“消耗”原始列表,并从中返回n个随机元素:

public static <T> List<T> nextRandomN(List<T> list, int n) {
  return new ArrayList<>(list).stream()
    .map(unused -> list.remove((int) (list.size() * Math.random())))
    .limit(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]
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.