产生一系列不重复随机数的问题

1 篇文章 0 订阅
1 篇文章 0 订阅

 最近需要考虑一个产生一系列不重复随机数的程序,搜索了一些资料,发现方法不太合意。于是对已有资料进行了总结,并提出自己的一些想法。

问题:
随机数的范围为[1,M]
所需生成个数为N(<=M)。
要求N个数之间互不相同。

需要注意的是,问题所要求的并不是一个随机序列(或者说,伪随机序列;
此处暂不讨论“真”“伪”随机问题,简单地认为计算机生成的随机数就是所要求的随机数)。随机
序列包含重复的随机数完全可能是正常的。本问题只是要求,每个随机生成时,是随机地从[1,M]中
挑选的,并且不与其他数相同。

此外,当然还要假定编程语言提供的随机数发生器生成的随机数范围(如C语言中为RAND_MAX)要比M
大。

首先在资料中碰到的一个问题是,很多人把N=M时的特殊情况,冠以本问题的标题。实际上当N=M时,应该
叫做“混洗”或者“洗牌”问题。这个问题有很多成熟的讨论。比如在《计算机程序设计艺术》3.4.2小节“
随机抽样和洗牌”中有对该问题的讨论,并提供了一个算法(算法P)。如下,
算法P(洗牌)设X1, X2, ..., Xt是要洗的t个数的一个集合。
P1.[初始化] 置j <-- t.
P2.[生成U] 生成在0和1之间一致分布的随机数U。
P3.[交换] 置k <-- floor[j*U] + 1 (floor为下取整函数,即取比j*U小的最大整数)。(现在
k是1与j之间的一个随机整数)交换Xk <--> Xj。
P4.[减小j] j减1。如果j>1,则返回步骤P2。
一般语言中随机数生成函数如C语言中的rand()会返回一个整数U,此时用U以j为模进行模运算也是可行的。
则 k <-- U % j + 1,其中%表示求模运算。
但是有个资料建议最好不要使用求模运算。比如求0到n之间的一个随机数,最好使用
j = (int)(n*(rand()/(RAND_MAX+1.0)))这种方法。该资料还给出一个据说是从MSDN上找到的例子
(crt_rand.c),生成给定范围内的随机数方法为
(int) ( (double) rand() / (double) RAND_MAX ) * (RANGE_MAX - RANGE_MIN + 1) + RANGE_MIN (原文的表达式似
有误)
虽然《计算机程序设计艺术》3.4.1小节习题3讨论了这个问题,
但是似乎是要求一个特殊操作的支持,对于一般语言,并未特别强调求模运算不行。

下面真正讨论一下本问题(N<M)。思路其实很简单,关键在于如何避免重复和实现上的效率问题。

很多资料中提供的一类方法的核心思想都是将整个随机数范围在数组中“物化”出来。有的用列表物化,
生成一个随机数之后,就将它从列表中删除;剩下的随机数在新的列表选取;如此可以避免重复。
有的用数组物化,对数组做一个混洗,然后从中挑选(连续截取一部分)所需数目的随机数即可。
这些方法可以避免重复,
但是将整个随机数范围都“物化”出来,其空间开销太大了,尤其在范围较大的时候,更是如此。因此
只适合随机数范围较小的情况。
或者将数组物化出来之后,借助上面洗牌算法P的处理方法。先随机选取一个,然后将被选中位置的
数与数组最后的数交换,然后将随机选取的下标范围减一,在这个下标范围再选下一个。反复如此,
可以导致不重复。如下(a[M]为物化后的的数组)。
int a[M];

for (i=0;i<M; i++) a[i] = i;
for (i=0;i<N; i++)
{
生成r为0到M-1-i范围内的随机数;
交换a[r]和a[M-1-i];
}
则a[M-N]到a[M-1]就是所需的N个随机数。我认为如果一定要物化随机数范围,那么最好使用这种方法。
该方法无需判重,每次都是从剩下的数中进行挑选。

另一大类方法并不物化随机数范围,而采用其他手段避免重复。其中最简单的一种是,每生成一个
随机数时,就检查它是否与已经产生的随机数重复,如果重复就重新产生以此。这种每产生一个数就
进行一次重复检查的做法,导致的时间开销是N*N的量级,在N比较大时,开销是比较大的。
对于N与M接近的情况,则
时间复杂性接近M的平方量级。而且有的资料提醒,这种方法理论上存在产生死循环的概率。
另外判断是否重复的方法是将为随机数生成范围内的每个数设置一个标志位,构成标志位数组,以此来
判断是否重复。这样等于也是物化一个数组,导致较大的空间开销,而且判重之后只是简单地的再生成
再判重,可以导致某个数要重复生成多次。

我自己考虑两种方法。
第一种:考虑随机生成数与数之间的间隔量,而非数值本身。
简单起见,随机数范围令为[1,M],需要生成随机数的个数为N个。
显然第一个随机数距离范围起点的最大间隔为M-N,最小为0,因此可以随机生成次间隔为0到M-N之间的数。
起点加上生成的间隔,即可得到第一个随机数,令为X1,
则选取第二个数时,问题变为从范围[X1+1,M]中选取N-1个随机数的问题,等价于范围在[1,M-X1]之中的问题,
离起点的最大间隔为M-X1-(N-1),最小为0,从而转化为选取第一个时处理的子问题。
之后以此类推。
这种方法的问题是,如果从多个序列(多次操作上述过程所得)来看,随机数的分布总体上是均匀的,但是
对于单次生成的某个序列,其均匀性极大地取决于前几次随机数生成的位置。只要第一次随机生成的间隔
较大,那么之后产生的数,其很可能就会集中在M附近,导致本次产生的序列不够均匀。

第二种:从去重的角度考虑,提高去重的效率;
先生成[1,M]范围内的N个随机数,然后将N个数排序,统计重复个数,令为D。如果有重复,那么在剩下的
数中挑。从剩下的数中挑的方法是,剩下M-(N-D)的个数,生成[1,M-(N-D)]范围的随机数i,表示挑到了
剩下的数中的第i个。然后扫描排序后的N个数,看这第i个剩下的数,在原来范围[1,M]中是第几个,找到之后,
将赋给原来N个数中的某个重复数,并重新恢复N的顺序。剩下的以此类推。
如果要求生成的N个数不按序排列,那么在对这N个数做个洗牌操作即可。
我将我的C语言代码贴出来,请各位指正。
// [1, M]: range
// a[N] : store the N random numbers
// cmp_int: the function for comparing two integers
// D: the number of duplicated numbers
// num_space: the space (OR range) of random numbers for
// current operation of generating random numbers

for (i=0; i < N; i++)
{

a[i] = 1 + rand () % M;
}

qsort (a, N, sizeof(int), cmp_int);

D = 0;
for (i=1; i < N; i++)
{
if (a[i] == a[i-1])
D++;
}

if ( D> 0 )
{
num_space -= (N - D);

i = 1;
while ( i < N)
{
if ( a[i] == a[i-1] )
{
j = i;
// maybe there is a group of duplicated numbers
while ( a[j] == a[i-1] && j < N )
{
rnd_temp = 1 + rand () % num_space;
curr_num_of_dups = 0;
for (k=0; k < N; k++)
{
if ( k > 0)
{
if ( a[k] == a[k-1] )
{
curr_num_of_dups++;
continue;
}
}
if ( a[k] - ((k+1) - curr_num_of_dups) < rnd_temp )
continue;
else
{
a[j] = rnd_temp + k - curr_num_of_dups;
break;
}
}
// restore the order after assign new value of a[j]
k = j;
while ( k > 0 && k < N -1 ) // when k==0 or k==N-1, need not move
{
if ( a[k] < a[k-1] )
{
a[k] <---> a[k-1];
k--;
}
else if ( a[k] > a[k+1])
{
a[k] <---> a[k+1];
k++;
}
else
break;
}
if ( k <= j) j++;
num_space--; // shrink the "num_space" by 1
}
i = j; // these group of duplicated numbers have been regenerated
}
else
i++;
}
}

第三种:有没有空间和时间效率更为高效的方法?拭目以待。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值