从顺序集合中获取随机元素

时间:2011-01-04 16:18:26

标签: java iterator

我与一个API进行了对话,该API为我提供了java.util.Iterator个集合。这意味着我可以迭代它,但我不能直接/随机访问元素。

现在我的问题:我想从这个集合中获取一个随机元素。我怎么做?我想我可以构建一个允许直接访问的新集合,但这不是一点消耗内存吗?我也可以迭代整个集合,并为每个元素“掷骰子”,看看我是否应该采用该元素并退出迭代或继续。但后来我需要集合的大小,我无法从迭代器中获得它。

提前致谢。

6 个答案:

答案 0 :(得分:10)

有一种方法可以在一次通过集合时执行此操作,该集合不使用大量额外内存(只是集合中一个元素的大小加上一个浮点数)。在伪代码中:

  • 迭代整个系列。
  • 对于每个项目,生成随机浮动。
  • 如果浮动是目前为止看到的最低(或最高,无关紧要)浮动,则将集合中的当前项目存储在临时变量中。 (还存储新的最低随机值。)
  • 一旦到达集合的末尾,temp变量中就会有一个随机项。

显然,这有一个缺点,就是每次调用它时都要遍历整个集合,但是你没有很多选择,只能面对你所面临的限制。

更新:此类问题的名称终于回复给了我。这称为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方法来置换列表。