【问题标题】:best way to pick a random subset from a collection?从集合中选择随机子集的最佳方法?
【发布时间】:2010-09-13 06:47:05
【问题描述】:

我在 Vector 中有一组对象,我想从中选择一个随机子集(例如,返回 100 个项目;随机选择 5 个)。在我的第一次(非常仓促的)传球中,我做了一个非常简单但可能过于聪明的解决方案:

Vector itemsVector = getItems();

Collections.shuffle(itemsVector);
itemsVector.setSize(5);

虽然这具有美观和简单的优点,但我怀疑它不会很好地扩展,即 Collections.shuffle() 必须至少为 O(n)。我不太聪明的选择是

Vector itemsVector = getItems();

Random rand = new Random(System.currentTimeMillis()); // would make this static to the class    

List subsetList = new ArrayList(5);
for (int i = 0; i < 5; i++) {
     // be sure to use Vector.remove() or you may get the same item twice
     subsetList.add(itemsVector.remove(rand.nextInt(itemsVector.size())));
}

关于从集合中抽取随机子集的更好方法有什么建议吗?

【问题讨论】:

  • 严格来说,您的代码假定您正在处理一个列表/向量。如果您处理任意集合,您首先必须将其所有项目提取到一个列表/向量/数组中,这可能非常昂贵。这是因为通常的洗牌算法只适用于列表/数组。
  • 我发现弗洛伊德的算法可以在所有子集上提供可证明的均匀分布,所以我强烈推荐 Eyal Schneider's answer,它链接到详细介绍它的帖子,包括。证明和实施。
  • itemsVector.remove 是 O(n) docs.oracle.com/javase/7/docs/api/java/util/…。我认为 O(k) 的运行时间是可能的。

标签: java algorithm collections random subset


【解决方案1】:

不过,您使用 Random 选择元素的第二种解决方案似乎是合理的:

【讨论】:

  • 感谢有关使用更好种子的提示;我会查看您发布的链接。完全同意使用 ArrayList 与 Vector;但是,这是一个返回 Vector 的第 3 方库,我无法控制返回的数据类型。谢谢!
  • 大声笑,我现在需要修复我的随机播放代码......我也使用 System.nanoTime() 作为我的种子!感谢您的精彩文章。
  • 听起来不错,但不是最好的方法。它比它需要的要慢。
【解决方案2】:

移除费用是多少?因为如果这需要将数组重写到新的内存块,那么您在第二个版本中完成了 O(5n) 操作,而不是您之前想要的 O(n)。

您可以创建一个设置为 false 的布尔数组,然后:

for (int i = 0; i < 5; i++){
   int r = rand.nextInt(itemsVector.size());
   while (boolArray[r]){
       r = rand.nextInt(itemsVector.size());
   }
   subsetList.add(itemsVector[r]);
   boolArray[r] = true;
}

如果您的子集比您的总大小小很多,则此方法有效。随着这些大小彼此接近(即大小的 1/4 或其他大小),您会在该随机数生成器上遇到更多冲突。在这种情况下,我会制作一个与您的较大数组大小相同的整数列表,然后打乱该整数列表,并从中提取第一个元素以获得您的(非冲突)索引。这样,您在构建整数数组时花费了 O(n),在洗牌中花费了另一个 O(n),但内部 while 检查器没有冲突,并且可能花费的潜在 O(5n) 更少。

【讨论】:

  • O(5N) === O(N);这就是大 O 符号的意义所在。但是,当您有两种方法时,均为 O(N),那么常数乘数和常数加法项变得重要(以及任何相关的次线性项)。
【解决方案3】:

我个人会选择您的初始实现:非常简洁。性能测试将显示它的可扩展性。我已经用一种被滥用的方法实现了一个非常相似的代码块,并且它的扩展性足够大。特定代码也依赖于包含超过 10,000 个项目的数组。

【讨论】:

    【解决方案4】:

    Jon Bentley 在“Programming Pearls”或“More Programming Pearls”中讨论了这一点。您需要小心您的 N of M 选择过程,但我认为显示的代码可以正常工作。与其随机打乱所有项目,不如随机打乱只打乱前 N 个位置 - 当 N 时,这是一个有用的节省

    Knuth 还讨论了这些算法 - 我相信这将是第 3 卷“排序和搜索”,但我的集合已打包等待搬家,所以我无法正式检查。

    【讨论】:

    • +1 击败了我的答案。我还写了关于前五个步骤执行随机洗牌的文章:从 1 到 M 中选择随机数,将第一个元素与该索引处的元素交换,从 2 到 M 中选择一个随机数,交换第二个元素,等等.
    • 感谢大家提供所有重要信息。虽然他们都有很棒的东西要添加,但我之所以选择这个,是因为这可能是我重构代码的方式: * set i = 0 * 从 i 中抓取随机元素 r 到 n * 将元素 @i 与元素 @ r * 交换i++ * 重复直到我得到我想要的
    【解决方案5】:

    @乔纳森,

    我相信这就是您所说的解决方案:

    void genknuth(int m, int n)
    {    for (int i = 0; i < n; i++)
             /* select m of remaining n-i */
             if ((bigrand() % (n-i)) < m) {
                 cout << i << "\n";
                 m--;
             }
    }
    

    它位于 Jon Bentley 的 Programming Pearls 第 127 页,基于 Knuth 的实现。

    编辑:我刚刚在第 129 页看到了进一步的修改:

    void genshuf(int m, int n)
    {    int i,j;
         int *x = new int[n];
         for (i = 0; i < n; i++)
             x[i] = i;
         for (i = 0; i < m; i++) {
             j = randint(i, n-1);
             int t = x[i]; x[i] = x[j]; x[j] = t;
         }
         sort(x, x+m);
         for (i = 0; i< m; i++)
             cout << x[i] << "\n";
    }
    

    这是基于“......我们只需要随机播放数组的前 m 个元素......”的想法

    【讨论】:

      【解决方案6】:
      Set<Integer> s = new HashSet<Integer>()
      // add random indexes to s
      while(s.size() < 5)
      {
          s.add(rand.nextInt(itemsVector.size()))
      }
      // iterate over s and put the items in the list
      for(Integer i : s)
      {
          out.add(itemsVector.get(i));
      }
      

      【讨论】:

      • 如果它没有概率运行时会很棒,当 n 接近集合的大小时它会增加很多......
      【解决方案7】:

      几周前我写了an efficient implementation of this。它在 C# 中,但翻译成 Java 很简单(基本上是相同的代码)。好的一面是它也完全没有偏见(一些现有的答案不是)-a way to test that is here。

      它基于 Fisher-Yates shuffle 的 Durstenfeld 实现。

      【讨论】:

      • 很棒的文章。我认为可以用来改进原始问题中的代码的一个要点是交换元素而不是删除它们。这样可以避免在删除元素时不得不折叠列表而导致性能损失。
      • 这接近于仅链接的答案 - 有人可以用相关代码更新吗?
      • 链接已损坏。
      • 感谢 WaybackMachine 的强大功能,您可以找到一份副本here
      【解决方案8】:

      如果你试图从 n 的列表中选择 k 个不同的元素,你上面给出的方法将是 O(n) 或 O(kn),因为从 Vector 中删除一个元素会导致 arraycopy 转移所有元素向下。

      由于您要求最好的方法,这取决于您可以对输入列表执行什么操作。

      如果可以接受修改输入列表,如您的示例中那样,那么您可以简单地将 k 个随机元素交换到列表的开头并在 O(k) 时间内返回它们,如下所示:

      public static <T> List<T> getRandomSubList(List<T> input, int subsetSize)
      {
          Random r = new Random();
          int inputSize = input.size();
          for (int i = 0; i < subsetSize; i++)
          {
              int indexToSwap = i + r.nextInt(inputSize - i);
              T temp = input.get(i);
              input.set(i, input.get(indexToSwap));
              input.set(indexToSwap, temp);
          }
          return input.subList(0, subsetSize);
      }
      

      如果列表必须以与开始时相同的状态结束,您可以跟踪您交换的位置,然后在复制您选择的子列表后将列表恢复到其原始状态。这仍然是一个 O(k) 的解决方案。

      但是,如果您根本无法修改输入列表并且 k 远小于 n(例如 100 中的 5),那么最好不要每次都删除选定的元素,而只需选择每个元素,如果你曾经得到一个副本,把它扔掉并重新选择。这会给你 O(kn / (n-k)) 当 n 支配 k 时仍然接近 O(k)。 (例如,如果 k 小于 n / 2,则它减少到 O(k))。

      如果 k 不以 n 为主,并且你不能修改列表,你不妨复制你的原始列表,并使用你的第一个解决方案,因为 O(n) 将和 O(k) 一样好。

      正如其他人所指出的,如果您依赖于每个子列表都可能(且无偏见)的强随机性,那么您肯定需要比java.util.Random 更强大的东西。见java.security.SecureRandom。

      【讨论】:

        【解决方案9】:

        This 是关于 stackoverflow 的一个非常相似的问题。

        总结我最喜欢的那个页面的答案(来自用户 Kyle 的第一个):

        • O(n) 解决方案:遍历您的列表,并以概率 (#needed / #remaining) 复制出一个元素(或对其的引用)。示例:如果 k = 5 且 n = 100,那么您采用概率为 5/100 的第一个元素。如果你复制那个,那么你选择下一个概率为 4/99;但如果你没有拿第一个,概率是 5/99。
        • O(k log k) 或 O(k2):构建 k 个索引的排序列表({0, 1, ..., n-1} 中的数字) 通过随机选择一个 =43,那么您将其加 1。所以如果你的第二个选择是 50,那么你加 1,你就有 {43, 51}。如果您的下一个选择是 51,则将 2 添加到它以获得 {43, 51, 53}。

        这是一些伪python -

        # Returns a container s with k distinct random numbers from {0, 1, ..., n-1}
        def ChooseRandomSubset(n, k):
          for i in range(k):
            r = UniformRandom(0, n-i)                 # May be 0, must be < n-i
            q = s.FirstIndexSuchThat( s[q] - q > r )  # This is the search.
            s.InsertInOrder(q ? r + q : r + len(s))   # Inserts right before q.
          return s 
        

        我是说时间复杂度是 O(k2) 或 O(k log k) 因为它取决于您搜索和插入的速度有多快你的容器。如果 s 是一个普通列表,那么其中一个操作是线性的,你得到 k^2。但是,如果你愿意将 s 构建为平衡二叉树,则可以节省 O(k log k) 时间。

        【讨论】:

        • 这些都不错,但不是最好的方法。它可以在 O(k) 内完成。
        • 这些不会与原始数组混淆。在不操纵原始数组的情况下,我还没有看到任何解决方案也能做到这一点。
        • 我在上面添加了这样的解决方案。只要 k 远小于 n,你最好从列表中选择随机元素,然后扔掉重复项,直到你得到 k。
        • 如果您使用散列集快速检查冲突,这是一个非常有用的算法。但是从理论分析来看,最坏的情况实际上是 O(infinity) 因为你没有保证碰撞次数的限制;非散列版本仍然需要 O(log k) 每次碰撞检查=k log k 总计。
        • 确实,您显然应该使用散列集来检查冲突。由于我们处理的是随机算法,因此分析输入的最坏情况的复杂性很重要,但随机值的预期情况。
        【解决方案10】:

        我认为这里不会出现两个解决方案 - 对应的内容很长,并且包含一些链接,但是,我认为并非所有帖子都与从集合中选择 K 元素的子集的问题有关N 个元素。 [“集合”是指数学术语,即所有元素出现一次,顺序不重要]。

        解决方案 1:

        //Assume the set is given as an array:
        Object[] set ....;
        for(int i=0;i<K; i++){
        randomNumber = random() % N;
            print set[randomNumber];
            //swap the chosen element with the last place
            temp = set[randomName];
            set[randomName] = set[N-1];
            set[N-1] = temp;
            //decrease N
            N--;
        }
        

        这看起来与丹尼尔给出的答案相似,但实际上却大不相同。它的运行时间为 O(k)。

        另一个解决方案是使用一些数学: 将数组索引视为 Z_n,因此我们可以随机选择 2 个数字,x 与 n 互质,即 chhose gcd(x,n)=1,另一个,a,它是“起点” - 然后是系列: a % n,a+x % n, a+2*x % n,...a+(k-1)*x%n 是不同数字的序列(只要 k

        【讨论】:

          猜你喜欢
          • 2010-11-03
          • 2013-03-28
          • 1970-01-01
          • 2016-09-24
          • 1970-01-01
          • 2010-12-11
          • 2012-01-30
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多