【问题标题】:Get a random element from a sequential collection从顺序集合中获取随机元素
【发布时间】:2011-01-04 16:18:26
【问题描述】:

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

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

提前致谢。

【问题讨论】:

  • 集合通常不应该是实现Iterator的类。
  • 你的收藏是java.util.Collection吗?
  • 内存消耗应该不会那么大。新集合只保存指向实际数据的指针,因此新集合对象的大小!=集合的大小。
  • @Jonathan B 在病态的情况下,集合包含许多重复元素或nulls,这可能会使集合的ArrayList 相对较大。
  • @thejh 这是我拙劣的写作,谢谢提及。我已经更新了问题。

标签: java iterator


【解决方案1】:

有一种方法可以在不使用大量额外内存的情况下一次性遍历集合(只需集合中一个元素的大小加上浮点数)。在伪代码中:

  • 遍历集合。
  • 为每个项目生成一个随机浮点数。
  • 如果浮点数是您目前看到的最低(或最高,没关系),请将集合中的当前项存储在一个临时变量中。 (同时存储新的最低随机值。)
  • 当您到达集合的末尾时,临时变量中有一个随机项。

显然,这样做的缺点是每次调用它时都要遍历整个集合,但是对于所面临的约束,您没有太多选择。

更新:这类问题的名称终于回到了我的身边。这称为Reservoir sampling

【讨论】:

  • 与我的解决方案非常相似(除了我不使用浮点数(顺便说一句,int 会更好))。
  • @Tom:这看起来确实是基本相同的想法。为什么int 更好呢?
  • @Bill the Lizard 对于给定的位数,int 可以为您提供更大的值分布。不必处理所有 IEEE 胡言乱语。
  • @Tom:哦,那个。我期待一些关于Random 的神秘琐事,但我还不知道。 :)
  • 我可能算错了,但我认为这不会在列表中统一选择。为了证明这一点,想想这对 3 个项目是如何工作的。第一项以 100% 的概率被选中。第二项以 50% 的概率被选中(到目前为止还不错),但第三项只会以 25% 的概率被选中,而不是应有的 1/3 概率。
【解决方案2】:

在迭代时,您知道您已经迭代了多少个对象,因此您知道当前元素是随机选择的元素的概率。所以你只需要保持一个计数和当前随机选择的项目。

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

(堆栈溢出免责声明:未编译,当然也未测试。)

另请参阅 Java Puzzlers 中有关 Collections.shuffle 的部分。

【讨论】:

  • 我不知道这是随机的:随着每次迭代,random.nextInt(count) == 0 的概率越来越低。
  • 当我传入一个包含一个项目的列表时,会有一次迭代。 count 获取值 2。在一半的情况下,null 将返回一个包含一个元素的列表,对吗?所以这是错误的。
  • @tulskly 是的,当你到第十个元素时,它有被选为 1/10 的概率。
  • @thejh 第一个元素是代码中的一个特例。 (它可以在循环中完成,但那将是 icky 代码。)
  • @MRalwasser 您可能需要再考虑一下。对于每个下一个元素,前一个选定项目有可能被替换。结果很公平。
【解决方案3】:

唯一安全的解决方案(如果不知道/保证没有更多信息)是您描述的方式: 从Iterator 创建一个List 并选择一个随机元素。

如果底层集合的大小始终相同,您可以平均减少一半的工作量 - 只需在随机迭代次数后使用 Iterator.next() 之后获得的元素。

顺便说一句:你真的在使用实现java.util.Iterator的集合吗?

【讨论】:

    【解决方案4】:

    这取决于要求,如果集合的大小不是很大,那么就这样做,否则你应该迭代并使用你提到的骰子方法

    List<Object> list = Arrays.asList(yourCollection.toArray(new Object[0]));
    result = list.get(new Random().nextInt(list.size()));
    

    【讨论】:

      【解决方案5】:

      用于生成加权测试数据。效率不高,但很容易

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

      【讨论】:

        【解决方案6】:

        如果您确实没有任何随机访问权限,并且您有一个非常大的列表,因此您无法复制它,那么您可以执行以下操作:

        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 方法来置换列表。

        【讨论】:

        • 和我的解决方案差不多。
        • 是的。你在我打字的时候添加了它:-)。
        猜你喜欢
        • 2022-01-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-04-18
        • 2014-08-25
        • 2010-09-12
        相关资源
        最近更新 更多