我与一个API进行了对话,该API为我提供了java.util.Iterator
个集合。这意味着我可以迭代它,但我不能直接/随机访问元素。
现在我的问题:我想从这个集合中获取一个随机元素。我怎么做?我想我可以构建一个允许直接访问的新集合,但这不是一点消耗内存吗?我也可以迭代整个集合,并为每个元素“掷骰子”,看看我是否应该采用该元素并退出迭代或继续。但后来我需要集合的大小,我无法从迭代器中获得它。
提前致谢。
答案 0 :(得分:10)
有一种方法可以在一次通过集合时执行此操作,该集合不使用大量额外内存(只是集合中一个元素的大小加上一个浮点数)。在伪代码中:
显然,这有一个缺点,就是每次调用它时都要遍历整个集合,但是你没有很多选择,只能面对你所面临的限制。
更新:此类问题的名称终于回复给了我。这称为Reservoir sampling。
答案 1 :(得分:7)
迭代时,您知道已经迭代了多少个对象,因此您知道当前元素是随机选择的概率。所以你只需要保持一个计数和当前随机选择的项目。
public static <T> T selectRandom(final Iterator<T> iter, final Random random) {
if (!iter.hasNext()) {
throw new IllegalArgumentException();
}
if (random == null) {
throw new NullPointerException();
}
T selected = iter.next();
int count = 1;
while (iter.hasNext()) {
final T current = iter.next();
++count;
if (random.nextInt(count) == 0) {
selected = current;
}
}
return selected;
}
(Stack Overflow免责声明:未编译,当然未经过测试。)
另请参阅Java Puzzlers中有关Collections.shuffle
的部分。
答案 2 :(得分:2)
唯一安全的解决方案(如果没有进一步的信息已知/保证)就是你所描述的方式:
从List
创建Iterator
并选择一个随机元素。
如果底层集合的大小始终相同,您可以将平均工作量减少一半 - 只需使用在随机迭代次数之后在Iterator.next()之后得到的元素。
BTW :你真的使用的是一个实现java.util.Iterator
的集合吗?
答案 3 :(得分:1)
这取决于要求,如果集合的大小不是很大,那么这样做,否则你应该迭代并使用你提到的骰子方法
List<Object> list = Arrays.asList(yourCollection.toArray(new Object[0]));
result = list.get(new Random().nextInt(list.size()));
答案 4 :(得分:1)
用于生成加权测试数据。它效率不高但很容易
class ProbabilitySet<E> {
Set<Option<E>> options = new HashSet<Option<E>>();
class Option<E> {
E object;
double min;
double max;
private Option(E object, double prob) {
this.object = object;
min = totalProb;
max = totalProb + prob;
}
@Override
public String toString() {
return "Option [object=" + object + ", min=" + min + ", max=" + max + "]";
}
}
double totalProb = 0;
Random rnd = new Random();
public void add(E object, double probability){
Option<E> tuple = new Option<E>(object, probability);
options.add(tuple);
totalProb += probability;
}
public E getRandomElement(){
double no = rnd.nextDouble() * totalProb;
for (Option<E> tuple : options) {
if (no >= tuple.min && no < tuple.max){
return tuple.object;
}
}
return null; // if this happens sumfink is wrong.
}
@Override
public String toString() {
return "ProbabilitySet [options=" + options + ", totalProb=" + totalProb + "]";
}
}
注意:概率参数相对于总数而言不是1.0
用法:
public static void main(String[] args) {
ProbabilitySet<String> stati = new ProbabilitySet<String>();
stati.add("TIMEOUT", 0.2);
stati.add("FAILED", 0.2);
stati.add("SUCCESSFUL", 1.0);
for (int i = 0; i < 100; i++) {
System.out.println(stati.getRandomElement());
}
}
答案 5 :(得分:0)
如果您确实没有任何随机访问权限,并且您有一个非常大的列表,因此您无法复制它,那么您可以执行以下操作:
int n = 2
iterator i = ...
Random rand = new Random();
Object candidate = i.next();
while (i.hasNext()) {
if (rand.nextInt(n)) {
candidate = i.next();
} else {
i.next();
}
n++;
}
return candidate;
这将保留列表中的随机元素,但需要遍历整个列表。如果你想要一个真正统一分布的价值,你别无选择,只能这样做。
或者,如果项目数量很少,或者您想要随机排列未知大小的列表(换句话说,您希望以随机顺序访问列表中的所有元素),那么我建议复制所有对新列表的引用(除非您只存储引用,否则这将不会占用大量内存,除非您有数百万个项目)。然后使用get随机整数或使用标准java.util.Collections shuffle方法来置换列表。