问题提供了所有必要的数据: 如何在给定区间 [0,N-1] 内生成一个由K个非重复整数组成的有效算法。如果 K 很大并且接近于 N,那么朴素算法(生成随机数并在将其添加到序列之前查找它们是否已经存在)是非常昂贵的。
Efficiently selecting a set of random elements from a linked list 提供的算法似乎比必要的复杂,并且需要一些实现。我刚刚找到了另一种算法,只需在单次遍历中知道所有相关参数即可完成工作。
问题提供了所有必要的数据: 如何在给定区间 [0,N-1] 内生成一个由K个非重复整数组成的有效算法。如果 K 很大并且接近于 N,那么朴素算法(生成随机数并在将其添加到序列之前查找它们是否已经存在)是非常昂贵的。
Efficiently selecting a set of random elements from a linked list 提供的算法似乎比必要的复杂,并且需要一些实现。我刚刚找到了另一种算法,只需在单次遍历中知道所有相关参数即可完成工作。
(defun sample-list (n list &optional (length (length list)) result)
(cond ((= length 0) result)
((< (* length (random 1.0)) n)
(sample-list (1- n) (cdr list) (1- length)
(cons (car list) result)))
(t (sample-list n (cdr list) (1- length) result))))
这里有一种不使用递归且适用于所有类型序列的实现:
(defun sample (n sequence)
(let ((length (length sequence))
(result (subseq sequence 0 n)))
(loop
with m = 0
for i from 0 and u = (random 1.0)
do (when (< (* (- length i) u)
(- n m))
(setf (elt result m) (elt sequence i))
(incf m))
until (= m n))
result))
Python库中的random模块使其变得异常易用和高效:
from random import sample
print sample(xrange(N), K)
sample
函数从给定序列中返回一个包含K个独特元素的列表。
xrange
是一个“列表仿真器”,它的行为类似于连续数字的列表,但是不会在内存中创建它,这使得类似于这样的任务超级快速。
random.shuffle
? - Tobias Kienzlerrandom
中的sample
方法与range
一起使用,即使只使用单个周期,速度也比TEA更快。此外,当仅使用v0
作为输出时,我偶尔会得到重复项。对于该实验,我创建了一个基于TEA的数字生成器,并初始化和计算了10,000组2048个数字,并出现了6个生成重复项的情况。也许多个周期会有所帮助,但即使对于一个周期,它已经比random.sample
慢了,后者还保证了唯一的数字。 - Stefan Fabian /* generate N sorted, non-duplicate integers in [0, max] */
int *generate(int n, int max) {
int i, m, a;
int *g = (int *)calloc(n, sizeof(int));
if (!g) return 0;
m = 0;
for (i = 0; i < max; i++) {
a = random_in_between(0, max - i);
if (a < n - m) {
g[m] = i;
m++;
}
}
return g;
}
有没有人知道我能在哪里找到更多像这样的宝石?
0...N-1
,并填充a[i]=i
。K
项。J=N-1
0...J
(比如R
)a[R]
和a[J]
R
可能等于J
,因此元素可能与自身交换J
中减去1
并重复。K
个最后元素。步骤1:生成整数列表。
步骤2:执行Knuth Shuffle。
请注意,您不需要对整个列表进行洗牌,因为Knuth Shuffle算法允许您仅应用n次洗牌,其中n是要返回的元素数量。生成列表仍将花费与列表大小成比例的时间,但您可以重复使用现有列表以满足任何未来的洗牌需求(假设大小保持不变),而无需在重新启动洗牌算法之前预先洗牌部分洗牌列表。
Knuth Shuffle的基本算法是从整数列表开始。然后,您将第一个整数与列表中的任何数字交换,并返回当前(新)第一个整数。然后,您将第二个整数与列表中的任何数字(除第一个数字外)交换,并返回当前(新)第二个整数。然后...等等...
这是一个非常简单的算法,但请注意,在执行交换时,请确保包括列表中的当前项目,否则您将破坏该算法。
我的解决方案是以C++为导向的,但我相信它可以翻译成其他语言,因为它非常简单。
这个解决方案只涉及两个循环迭代,没有哈希表查找或任何类似的东西。所以在实际代码中:
// Assume K is the highest number in the list
std::vector<int> sorted_list;
std::vector<int> random_list;
for(int i = 0; i < K; ++i) {
sorted_list.push_back(i);
}
// Loop to K - 1 elements, as this will cause problems when trying to erase
// the first element
while(!sorted_list.size() > 1) {
int rand_index = rand() % sorted_list.size();
random_list.push_back(sorted_list.at(rand_index));
sorted_list.erase(sorted_list.begin() + rand_index);
}
// Finally push back the last remaining element to the random list
// The if() statement here is just a sanity check, in case K == 0
if(!sorted_list.empty()) {
random_list.push_back(sorted_list.at(0));
}
/* Sampling according to [Vitter87].
*
* Bibliography
* [Vitter 87]
* Jeffrey Scott Vitter,
* An Efficient Algorithm for Sequential Random Sampling
* ACM Transactions on MAthematical Software, 13 (1), 58 (1987).
*/
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <string>
#include <iostream>
#include <iomanip>
#include <boost/random/linear_congruential.hpp>
#include <boost/random/variate_generator.hpp>
#include <boost/random/uniform_real.hpp>
using namespace std;
// This is a typedef for a random number generator.
// Try boost::mt19937 or boost::ecuyer1988 instead of boost::minstd_rand
typedef boost::minstd_rand base_generator_type;
// Define a random number generator and initialize it with a reproducible
// seed.
// (The seed is unsigned, otherwise the wrong overload may be selected
// when using mt19937 as the base_generator_type.)
base_generator_type generator(0xBB84u);
//TODO : change the seed above !
// Defines the suitable uniform ditribution.
boost::uniform_real<> uni_dist(0,1);
boost::variate_generator<base_generator_type&, boost::uniform_real<> > uni(generator, uni_dist);
void SequentialSamplesMethodA(int K, int N)
// Outputs K sorted random integers out of 0..N, taken according to
// [Vitter87], method A.
{
int top=N-K, S, curr=0, currsample=-1;
double Nreal=N, quot=1., V;
while (K>=2)
{
V=uni();
S=0;
quot=top/Nreal;
while (quot > V)
{
S++; top--; Nreal--;
quot *= top/Nreal;
}
currsample+=1+S;
cout << curr << " : " << currsample << "\n";
Nreal--; K--;curr++;
}
// special case K=1 to avoid overflow
S=floor(round(Nreal)*uni());
currsample+=1+S;
cout << curr << " : " << currsample << "\n";
}
void SequentialSamplesMethodD(int K, int N)
// Outputs K sorted random integers out of 0..N, taken according to
// [Vitter87], method D.
{
const int negalphainv=-13; //between -20 and -7 according to [Vitter87]
//optimized for an implementation in 1987 !!!
int curr=0, currsample=0;
int threshold=-negalphainv*K;
double Kreal=K, Kinv=1./Kreal, Nreal=N;
double Vprime=exp(log(uni())*Kinv);
int qu1=N+1-K; double qu1real=qu1;
double Kmin1inv, X, U, negSreal, y1, y2, top, bottom;
int S, limit;
while ((K>1)&&(threshold<N))
{
Kmin1inv=1./(Kreal-1.);
while(1)
{//Step D2: generate X and U
while(1)
{
X=Nreal*(1-Vprime);
S=floor(X);
if (S<qu1) {break;}
Vprime=exp(log(uni())*Kinv);
}
U=uni();
negSreal=-S;
//step D3: Accept ?
y1=exp(log(U*Nreal/qu1real)*Kmin1inv);
Vprime=y1*(1. - X/Nreal)*(qu1real/(negSreal+qu1real));
if (Vprime <=1.) {break;} //Accept ! Test [Vitter87](2.8) is true
//step D4 Accept ?
y2=0; top=Nreal-1.;
if (K-1 > S)
{bottom=Nreal-Kreal; limit=N-S;}
else {bottom=Nreal+negSreal-1.; limit=qu1;}
for(int t=N-1;t>=limit;t--)
{y2*=top/bottom;top--; bottom--;}
if (Nreal/(Nreal-X)>=y1*exp(log(y2)*Kmin1inv))
{//Accept !
Vprime=exp(log(uni())*Kmin1inv);
break;
}
Vprime=exp(log(uni())*Kmin1inv);
}
// Step D5: Select the (S+1)th record
currsample+=1+S;
cout << curr << " : " << currsample << "\n";
curr++;
N-=S+1; Nreal+=negSreal-1.;
K-=1; Kreal-=1; Kinv=Kmin1inv;
qu1-=S; qu1real+=negSreal;
threshold+=negalphainv;
}
if (K>1) {SequentialSamplesMethodA(K, N);}
else {
S=floor(N*Vprime);
currsample+=1+S;
cout << curr << " : " << currsample << "\n";
}
}
int main(void)
{
int Ntest=10000000, Ktest=Ntest/100;
SequentialSamplesMethodD(Ktest,Ntest);
return 0;
}
$ time ./sampling|tail
在我的笔记本上输出如下
99990 : 9998882
99991 : 9998885
99992 : 9999021
99993 : 9999058
99994 : 9999339
99995 : 9999359
99996 : 9999411
99997 : 9999427
99998 : 9999584
99999 : 9999745
real 0m0.075s
user 0m0.060s
sys 0m0.000s
蓄水池抽样版本非常简单:
my $N = 20;
my $k;
my @r;
while(<>) {
if(++$k <= $N) {
push @r, $_;
} elsif(rand(1) <= ($N/$k)) {
$r[rand(@r)] = $_;
}
}
print @r;
这是从标准输入中随机选择的 $N 行。如果您不使用文件中的行,则将 <>/$_ 替换为其他内容,但这是一个非常简单的算法。