如何从中取n个随机元素ArrayList<E>
?理想情况下,我希望能够连续调用该take()
方法以获取另一个x元素,而无需替换。
如何从中取n个随机元素ArrayList<E>
?理想情况下,我希望能够连续调用该take()
方法以获取另一个x元素,而无需替换。
Answers:
两种主要方式。
List<Foo> list = createItSomehow();
Random random = new Random();
Foo foo = list.get(random.nextInt(list.size()));
但是,不能保证连续的n
调用返回唯一的元素。
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()
改用。
到现在为止,大多数提议的解决方案都建议通过检查唯一性来进行完整列表混洗或连续随机选择,并在需要时重试。
但是,我们可以利用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());
}
如果您要从列表中连续选择n个元素,并且能够一遍又一遍地替换而不需要替换,则最好是随机排列这些元素,然后以n个块的形式取出块。如果您随机排列列表,则可以保证您选择的每个块的统计随机性。也许最简单的方法就是使用Collections.shuffle
。
简单明了
// 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;
arrayList
和targetList
。
这样做的一种公平方法是遍历列表,在第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;
}
(这段代码是从我前一段时间写的从列表中随机抽取样本的页面复制的。)
使用以下类:
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;
}
}
继续选择一个随机元素,并确保不再选择相同的元素:
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;
}
从理论上讲,这可以无休止地运行,但实际上是可以的。您越接近整个原始列表,显然它的运行时间就越慢,但这不是选择随机子列表的重点,不是吗?
如其他答案Collections.shuffle
所述,由于复制,当源列表很大时效率不是很高。这是一个Java 8单行代码:
码:
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)
。
下面的类从任何类型的列表中检索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)));
}
}
此解决方案不会修改原始列表,也不会随着列表大小而增加复杂性。
要从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;
}
所有这些答案都需要可修改的列表,否则会产生性能问题
这是一个快速的代码段,它需要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
}
下面的方法返回一个新的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]