我正在寻找最有效的算法来随机选择一组n个不同的整数,其中所有整数都在某个范围[0..maxValue]。
约束:
我最初的想法是构造一个整数列表[0..maxValue]然后随机提取n个元素而不进行替换。但这似乎效率很低,特别是如果maxValue很大的话。
有更好的解决方案吗?
答案 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
并且您不希望生成整个数组,那么您可以使用此算法:
l
,最初为空。x
之间选择一个随机数maxValue
- (l
中的元素)l
中的每个号码,如果小于或等于x
,请将{1}添加到x
x
值添加到已排序的列表中并重复。如果n
非常靠近maxValue
,那么您可以随机选择结果中不的元素,然后找到该集合的补充。
这是另一种更简单但可能无限制执行时间的算法:
s
元素,最初为空。maxValue
之间随机选择一个数字。s
中,请将其添加到s
。s
有n
个元素。在实践中,如果n
很小且maxValue
很大,那么这对于大多数用途来说已经足够了。
答案 2 :(得分:2)
在不生成完整数组的情况下执行此操作的一种方法。
说我想从一组{x1,...,xn}中随机选择m个项目的子集,其中m <= n。
考虑元素x1。我以概率m / n将x1添加到我的子集中。
泡沫,冲洗,重复直至m = 0.
此算法为O(n),其中n是我必须考虑的项目数。
我更倾向于想象有一个O(m)算法,在每一步中你要考虑从可能性的“前面”中删除多少元素,但我还没有说服自己一个好的解决方案而且我有现在做一些工作!
答案 3 :(得分:2)
如果您从M
中选择N
个元素,则策略会根据M
与N
的顺序是否相同或更少(即小于关于N / log N)。
如果它们的大小相似,那么您可以浏览从1
到N
的每个项目。您可以跟踪到目前为止已经有多少项目(让我们称之为m
从您n
中选出的(M-m)/(N-n)
项目,然后您以概率{{ 1}}然后丢弃它。然后,您适当更新m
和n
并继续。这是一个O(N)
算法,其成本较低。
另一方面,如果M
明显小于N
,则重新采样策略是一个很好的策略。在这里,您需要对M
进行排序,以便您可以快速找到它们(这将花费您O(M log M)
时间 - 例如,将它们粘贴到树中。现在,您从1
到N
统一选取数字并将其插入列表中。如果发现碰撞,请再次选择。您将在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,可以返回0
到upto
之间的数字。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 。注意经验测试如何接近理论测试。