随机选择一组不同整数的最有效方法

时间:2010-09-15 22:48:54

标签: algorithm random language-agnostic combinations

我正在寻找最有效的算法来随机选择一组n个不同的整数,其中所有整数都在某个范围[0..maxValue]。

约束:

  • maxValue大于n,可能更大
  • 我不关心输出列表是否已排序
  • 必须以相同的概率选择所有整数

我最初的想法是构造一个整数列表[0..maxValue]然后随机提取n个元素而不进行替换。但这似乎效率很低,特别是如果maxValue很大的话。

有更好的解决方案吗?

8 个答案:

答案 0 :(得分:13)

这是一个最佳算法,假设我们被允许使用散列图。它运行在 O(n)时间和空间(而不是O(maxValue)时间,这太贵了)。

它基于Floyd的随机样本算法。有关详细信息,请参阅我的blog post。 代码是Java:

private static Random rnd = new Random();

public static Set<Integer> randomSample(int max, int n) {
    HashSet<Integer> res = new HashSet<Integer>(n);
    int count = max + 1;
    for (int i = count - n; i < count; i++) {
        Integer item = rnd.nextInt(i + 1);
        if (res.contains(item))
            res.add(i);
        else
            res.add(item);
    }
    return res;
}

答案 1 :(得分:7)

对于较小的maxValue值,以便在内存中生成所有整数的数组是合理的,那么除了仅执行第一个n步骤之外,您可以使用Fisher-Yates shuffle的变体。


如果n远小于maxValue并且您不希望生成整个数组,那么您可以使用此算法:

  1. 保留到目前为止所选号码的排序列表l,最初为空。
  2. 在0和x之间选择一个随机数maxValue - (l中的元素)
  3. 对于l中的每个号码,如果小于或等于x,请将{1}添加到x
  4. 将调整后的x值添加到已排序的列表中并重复。
  5. 如果n非常靠近maxValue,那么您可以随机选择结果中的元素,然后找到该集合的补充。


    这是另一种更简单但可能无限制执行时间的算法:

    1. 到目前为止,保留一组s元素,最初为空。
    2. 在0和maxValue之间随机选择一个数字。
    3. 如果该号码不在s中,请将其添加到s
    4. 返回第2步,直到sn个元素。
    5. 在实践中,如果n很小且maxValue很大,那么这对于大多数用途来说已经足够了。

答案 2 :(得分:2)

在不生成完整数组的情况下执行此操作的一种方法。

说我想从一组{x1,...,xn}中随机选择m个项目的子集,其中m <= n。

考虑元素x1。我以概率m / n将x1添加到我的子集中。

  • 如果我将x1添加到我的子集中,那么我将问题从{x2,...,xn}中选择(m - 1)个项目。
  • 如果我将x1添加到我的子集中,那么我将问题从{x2,...,xn}中选择m项。

泡沫,冲洗,重复直至m = 0.

此算法为O(n),其中n是我必须考虑的项目数。

我更倾向于想象有一个O(m)算法,在每一步中你要考虑从可能性的“前面”中删除多少元素,但我还没有说服自己一个好的解决方案而且我有现在做一些工作!

答案 3 :(得分:2)

如果您从M中选择N个元素,则策略会根据MN的顺序是否相同或更少(即小于关于N / log N)。

如果它们的大小相似,那么您可以浏览从1N的每个项目。您可以跟踪到目前为止已经有多少项目(让我们称之为m从您n中选出的(M-m)/(N-n)项目,然后您以概率{{ 1}}然后丢弃它。然后,您适当更新mn并继续。这是一个O(N)算法,其成本较低。

另一方面,如果M明显小于N,则重新采样策略是一个很好的策略。在这里,您需要对M进行排序,以便您可以快速找到它们(这将花费您O(M log M)时间 - 例如,将它们粘贴到树中。现在,您从1N统一选取数字并将其插入列表中。如果发现碰撞,请再次选择。您将在M/N时间内发生碰撞(实际上,您正在从1 / N到M / N进行整合),这将需要您再次选择(递归),因此您将期望{{1选择完成该过程。因此,此算法的费用大约为M/(1-M/N)

这些都是这样简单的方法,您可以实现这两种方法 - 假设您可以访问已排序的树 - 并根据将要选择的数字部分选择适当的方法。

(请注意,选择数字是对称的,未选中它们,因此如果O(M*(N/(N-M))*log(M))几乎等于M,那么您可以使用重采样策略,但选择这些数字不< / em> include;这可能是一场胜利,即使您必须推送所有差不多 - N数字,如果您的随机数生成很昂贵。)

答案 4 :(得分:1)

我的解决方案与Mark Byers相同。它需要O(n ^ 2)时间,因此当n远小于maxValue时它很有用。这是python中的实现:

def pick(n, maxValue):
    chosen = []
    for i in range(n):
        r = random.randint(0, maxValue - i)
        for e in chosen:
            if e <= r:
                r += 1
            else:
                break;
        bisect.insort(chosen, r)
    return chosen

答案 5 :(得分:1)

诀窍是使用shuffle的变体,或者换句话说是部分随机播放。

function random_pick( a, n ) 
{
  N = len(a);
  n = min(n, N);
  picked = array_fill(0, n, 0); backup = array_fill(0, n, 0);
  // partially shuffle the array, and generate unbiased selection simultaneously
  // this is a variation on fisher-yates-knuth shuffle
  for (i=0; i<n; i++) // O(n) times
  { 
    selected = rand( 0, --N ); // unbiased sampling N * N-1 * N-2 * .. * N-n+1
    value = a[ selected ];
    a[ selected ] = a[ N ];
    a[ N ] = value;
    backup[ i ] = selected;
    picked[ i ] = value;
  }
  // restore partially shuffled input array from backup
  // optional step, if needed it can be ignored
  for (i=n-1; i>=0; i--) // O(n) times
  { 
    selected = backup[ i ];
    value = a[ N ];
    a[ N ] = a[ selected ];
    a[ selected ] = value;
    N++;
  }
  return picked;
}

注意该算法在时间和空间中严格O(n),生成无偏选择(它是部分无偏见的改组)和不需要哈希(可能无法使用和/或通常隐藏其实现背后的复杂性,例如获取时间不是O(1),它可能在最坏的情况下甚至是O(n)

改编自here

答案 6 :(得分:0)

线性同余生成器模数maxValue + 1。我确定我以前写过这个答案,但我找不到它......

答案 7 :(得分:0)

更新:我错了。其输出不均匀分布。有关原因的详细信息为here

我认为下面的算法是最佳。即你不可能获得比这更好的表现。

为了从 m 数字中选择 n 数字,到目前为止提供的最佳算法如下所示。其最差的运行时复杂度是 O(n),并且只需要一个数组来存储原始数字。它会对原始数组中的第一个 n 元素进行部分洗牌,然后您选择第一个 n 洗牌后的数字作为解决方案。

这也是一个完全有效的C程序。你找到的是:

  • 功能getrand:这只是一个PRNG,可以返回0upto之间的数字。
  • 功能randselect:这是randmoly从 m 多个数字中选择 n 唯一数字的函数。这就是这个问题的关键所在。
  • 功能main:这只是为了演示其他功能的用途,以便您可以将其编译成程序并享受乐趣。
#include <stdio.h>
#include <stdlib.h>

int getrand(int upto) {
    long int r;
    do {
        r = rand();
    } while (r > upto);
    return r;
}

void randselect(int *all, int end, int select) {
    int upto = RAND_MAX - (RAND_MAX % end);
    int binwidth = upto / end;

    int c;
    for (c = 0; c < select; c++) {
        /* randomly choose some bin */
        int bin = getrand(upto)/binwidth;

        /* swap c with bin */
        int tmp = all[c];
        all[c] = all[bin];
        all[bin] = tmp;
    }
}

int main() {
    int end = 1000;
    int select = 5;

    /* initialize all numbers up to end */
    int *all = malloc(end * sizeof(int));
    int c;
    for (c = 0; c < end; c++) {
        all[c] = c;
    }

    /* select select unique numbers randomly */
    srand(0);
    randselect(all, end, select);
    for (c = 0; c < select; c++) printf("%d ", all[c]);
    putchar('\n');

    return 0;
}

Here是示例代码的输出,我从 8 数字池中随机输出 4 排列,持续100,000,000次。然后我使用那些许多排列来计算出每个唯一排列发生的概率。然后我按这个概率对它们进行排序。您注意到数字非常接近,我认为这意味着它是均匀分布的。理论概率应 1/1680 = 0.000595238095238095 。注意经验测试如何接近理论测试。