随机数的生成
在C语言(C++)中,随机数的生成主要依靠rand()函数实现,同时在程序中需要包含<cstdlib>以及<ctime>等头文件。
· 通过rand()函数生成随机数
普通的随机函数rand()可以生成[0,32767]范围内的伪随机数,借助系统时间产生的随机种子,可以实现等概率生成随机数。
如果我们需要[0,n-1]范围内的随机数,只需要用rand()对n取模就可以了。
#include<cstdio>
#include<cstdlib>
#include<ctime>
int main(){
srand(time(NULL));
int n=rand()%100+1;
printf("Random Number in Range [1,100] : %d\n",n);
return 0;
}
但是这样生成的随机数存在两个问题:
(1) 当n不是2的若干次幂时,产生的随机数不是等概率的。
(2) 由于rand()函数有上限,因此n必须小于等于RAND_MAX+1,也就是32768。
为什么会出现这样的问题?
举个例子:如果我们要生成[0,9999]之间的随机数,此时
=10000,根据上述做法,首先要等概率产生一个[0,32767]范围内的随机数
,然后将
对
取模得到随机数
。
对于
的某个取值
,若
落在区间[0,2767]内,则
=
,若
落在区间[2768,9999]内,则
=
。显然,对于不同区间内的数字来说,被选中的概率也是不同的,并且概率的比值接近于
。
如果这个范围再大一些,比如
=100000时,区间[32768,99999]内的数字出现的概率为0。
使用rand()函数生成100万个[0,9999]范围内的随机数,并统计每个长度为1000的区间内的随机数个数,测试结果验证了随机数的分布不均匀的情况:
如何解决这样的问题?
为了解决上述两个问题,我们可以将多个随机数拼接在一起,生成更大范围内的随机数。
· 通过多个随机数拼接生成随机数
假设通过rand()函数生成了两个[0,32767]范围内的随机数 和 ,我们可以令 = << + 生成一个[0,230-1]范围内的随机数。对于任意一个非负整数 ,都可以用唯一的非负整数对 来表示;对于任意一组非负整数对 ,都可以确定唯一的非负整数 。又因为 和 的取值相互独立,非负整数对 是等概率随机产生的,所以这样生成的非负整数 仍然是等概率的。
用类似的方法,我们可以等概率的生成[0,2k-1]范围内的随机数
。然后可以用
对
取模得到一个[0,n-1]范围内的随机数
。
虽然这样得到的
仍然不是等概率的,但是当
增大时,概率之间的差异会相对减少。记
=
,
=
,则区间[0,
-1]内的每个
出现的概率
=
,区间[
,
-1]内的每个
出现的概率
=
,概率之比为
。当
取100000,
取30时,计算得到
的值为10737,概率之间的差异不到0.0001。
C语言代码如下:
// 生成[0,n-1]之间的整数
long long Rand(long long n){
long long result=0;
// 这里的随机数上限是1e18,考虑时间效率与数据范围选择每次左移10位
for(int i=0;i<6;i++){
result=((result<<10)+rand()%1024)%n;
}
return result;
}
随机浮点数的生成只需要通过移动小数点就可以实现:
// 生成[0,1)之间的浮点数
double fRand(){
return Rand(1e15)*1e-15;
}
使用上述代码随机产生的20个整数如下图所示:
使用上述代码随机产生的20个浮点数如下图所示:
随机序列的生成
如果是带有重复元素的随机序列,可以直接把 个随机数拼接在一起,如果要去重的话就会稍微麻烦一些。
· 通过随机全排列生成随机序列
当随机序列的值域比较小的时候,这里假设元素的取值范围是[1, ],可以先产生 ~ 的随机排列,然后取出前 项即可。
随机排列的生成可以使用STL中的random_shuffle()函数,也可以用以下方法生成:
(1) 初始化长度为
的数组
[ ],其中第
个元素是
。
(2) 接下来逐个选出随机序列的元素。假设当前正在进行第
次循环,可以先在[0,
-
-1]的范围内产生一个随机数
,令
=
+
,然后交换a[i]与a[j]。
(3) 重复上述过程,直到选出
个元素。
对于元素x,被选出的概率
,其中
。当
时,每个元素一定会被选中,这种方法也可以用来随机打乱数组。
另外,上述过程的时间复杂度为
,空间复杂度为
。
以下是使用C++实现的随机序列生成方法:
template<typename T>
void shuffle(T array[],int len,int size){
for(int i=0;i<size;i++){
int j=i+Rand(len-i);
swap(array[i],array[j]);
}
return ;
}
· 通过set集合去重生成随机序列
如果随机序列的值域比较大,使用随机排列方法的空间消耗是无法接受的,我们可以使用set集合进行去重:
每次产生一个随机数
,然后加到set集合中去,如果元素
已经在set中存在,就不会被添加。重复上述过程直到set中元素的个数恰好为
。
接下来分析这种方法的时间效率:
设
表示已经放进
个元素时,剩余的期望操作步数。
首先可以得到
,以及状态转移方程
,进而得到递推关系
,由此推出:
令
,化简得到
。当
=
时,期望步数的上限为
。当
=
时,期望步数的上限为
。当
趋近于无穷大时,
趋近于
,期望步数会趋近于
。因此期望时间复杂度近似为
。
C语言代码如下:
#include<cstdio>
#include<cstdlib>
#include<ctime>
#include<set>
#include<algorithm>
using namespace std;
const int MAXN=1e5;
set<long long> arr;
set<long long>::iterator it;
long long Rand(long long n){
long long result=0;
for(int i=0;i<6;i++){
result=((result<<10)+rand()%1024)%n;
}
return result;
}
int main(){
srand(time(NULL));
int cnt=0;
clock_t start,end;
start=clock(); // 计时器开始
arr.clear();
while(arr.size()<MAXN){
arr.insert(Rand(1e12));
cnt+=1; // 记录循环执行次数
}
end=clock(); // 计时器停止
it=arr.begin();
printf("\n");
for(int i=0;i<20;i++){
printf("Random #%-2d : %10lld\n",i+1,*(it++));
}
printf(">> Number of Loops Executed : %d\n",cnt);
printf(">> Total Execute Time : %.2lfms\n",(double)(end-start)*1000.0/CLOCKS_PER_SEC);
return 0;
}
代码运行结果如下:
最后注意使用set生成的序列是有序的,需要提取到数组中并使用random_shuffle()函数重新打乱顺序。