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

时间:2011-01-15 20:44:08

标签: java algorithm random sampling

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

12 个答案:

答案 0 :(得分:95)

两种主要方式。

  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个唯一元素(假设列表本身包含唯一元素)。


  3. 如果你想知道是否有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()

答案 1 :(得分:26)

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

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

  

Durstenfeld的解决方案是移动&#34;击中&#34;数字到了最后   列表中的每一个都使用最后一个未编号的数字进行交换   迭代。

由于上述原因,我们不需要对整个列表进行随机播放,但运行循环的步骤与返回所需的元素数量相同。如果我们使用完美的随机函数,该算法可确保列表末尾的最后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());
}

答案 2 :(得分:10)

如果你想连续从列表中选择n个元素并且无需一遍又一遍地进行替换,那么最好随机置换元素,然后在n个块中取出块。如果您随机置换列表,则可以保证您选择的每个块的统计随机性。也许最简单的方法是使用Collections.shuffle

答案 3 :(得分: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;

答案 4 :(得分:5)

一个公平的方法是通过列表,在第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上写过的页面复制而来的。)

答案 5 :(得分: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;
    }
}

答案 6 :(得分: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;
}

从理论上讲,这可能会无休止地运行,但在实践中它很好。你越接近整个原始列表,它的运行时间越明显越明显,但这不是选择随机子列表的重点,是吗?

答案 7 :(得分: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)));
    }
}

答案 8 :(得分:0)

如其他答案所述,由于复制,当源列表很大时,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)

答案 9 :(得分: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;
}

答案 10 :(得分: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
}

答案 11 :(得分:0)

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