从集合中选择随机子集的最佳方法?

时间:2008-09-25 22:03:00

标签: java algorithm collections random subset

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

有关从集合中抽出随机子集的更好方法的任何建议吗?

10 个答案:

答案 0 :(得分:10)

Jon Bentley在'Programming Pearls'或'More Programming Pearls'中讨论了这个问题。您需要小心N的M选择过程,但我认为显示的代码可以正常工作。不是随机改变所有项目,你可以进行随机改组,只改组前N个位置 - 当N <&lt;&lt;微米。

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

答案 1 :(得分:8)

@Jonathan,

我相信这是你正在谈论的解决方案:

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 元素进行洗牌......”

答案 2 :(得分:4)

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

这是基于Durstenfeld实施的Fisher-Yates shuffle。

答案 3 :(得分:4)

如果你试图从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

答案 4 :(得分:2)

然而,使用随机选择元素的第二个解决方案看似合理:

答案 5 :(得分:0)

移除费用多少钱?因为如果需要将数组重写为新的内存块,那么你已经在第二个版本中完成了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)的成本,而在shuffle中有另一个O(n),但是没有来自内部的检查器和小于可能消耗的潜在O(5n)的冲突。

答案 6 :(得分:0)

我个人选择初步实施:非常简洁。性能测试将显示它的扩展程度。我已经在一个体面的滥用方法中实现了一个非常相似的代码块,并且它已经足够扩展。特定代码依赖于包含&gt; 10,000项的数组。

答案 7 :(得分:0)

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

答案 8 :(得分:0)

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

总结我最喜欢的答案(来自用户Kyle):

  • O(n)解决方案:遍历您的列表,并以概率(#needed / #remaining)复制元素(或其引用)。示例:如果k = 5且n = 100,则使用prob 5/100获取第一个元素。如果你复制那个,那么你选择下一个问题4/99;但如果你没有拿第一个,则概率为5/99。
  • O(k log k)或O(k 2 :构建k个索引的排序列表({0,1,...,n中的数字-1})通过随机选择一个数字&lt; n,然后随机选择一个数字&lt; n-1等。在每一步中,您需要重新调整您的选择以避免碰撞并保持概率均匀。例如,如果k = 5且n = 100,并且您的第一个选择是43,那么您的下一个选择是在[0,98]范围内,如果它是&gt; = 43,那么您将其添加1。所以如果你的第二个选择是50,那么你加1,你有{43,41}。如果您的下一个选择是51,则将 2 添加到其中以获得{43,41,53}。

这是一些伪蟒 -

# 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(k 2 O(k log k),因为它取决于您搜索和插入的速度有多快你的容器。如果s是普通列表,那么其中一个操作是线性的,你得到k ^ 2。但是,如果您愿意将s构建为平衡二叉树,则可以获得O(k log k)时间。

答案 9 :(得分:0)

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

Sol 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 <= n)。