随机数与随机序列生成

随机数的生成

在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]之间的随机数,此时 n n n=10000,根据上述做法,首先要等概率产生一个[0,32767]范围内的随机数 r r r,然后将 r r r n n n取模得到随机数 x x x
对于 x x x的某个取值 x 0 x_0 x0,若 x 0 x_0 x0落在区间[0,2767]内,则 P ( x P(x P(x= x 0 ) = 4 32767 x_0)=\frac{4}{32767} x0)=327674,若 x 0 x0 x0落在区间[2768,9999]内,则 P ( x P(x P(x= x 0 ) = 3 32767 x_0)=\frac{3}{32767} x0)=327673。显然,对于不同区间内的数字来说,被选中的概率也是不同的,并且概率的比值接近于 4 : 3 4:3 4:3
如果这个范围再大一些,比如 n n n=100000时,区间[32768,99999]内的数字出现的概率为0。

使用rand()函数生成100万个[0,9999]范围内的随机数,并统计每个长度为1000的区间内的随机数个数,测试结果验证了随机数的分布不均匀的情况:

随机数分布测试结果

如何解决这样的问题?
为了解决上述两个问题,我们可以将多个随机数拼接在一起,生成更大范围内的随机数。

· 通过多个随机数拼接生成随机数

假设通过rand()函数生成了两个[0,32767]范围内的随机数 x 1 x_1 x1 x 2 x_2 x2,我们可以令 y y y= ( x 1 (x_1 (x1<< 15 ) 15) 15)+ x 2 x_2 x2生成一个[0,230-1]范围内的随机数。对于任意一个非负整数 y y y,都可以用唯一的非负整数对 ( x 1 , x 2 ) (x_1,x_2) (x1,x2)来表示;对于任意一组非负整数对 ( x 1 , x 2 ) (x_1,x_2) (x1,x2),都可以确定唯一的非负整数 y y y。又因为 x 1 x_1 x1 x 2 x_2 x2的取值相互独立,非负整数对 ( x 1 , x 2 ) (x_1,x_2) (x1,x2)是等概率随机产生的,所以这样生成的非负整数 y y y仍然是等概率的。

用类似的方法,我们可以等概率的生成[0,2k-1]范围内的随机数 R R R。然后可以用 R R R n n n取模得到一个[0,n-1]范围内的随机数 x x x
虽然这样得到的 x x x仍然不是等概率的,但是当 k k k增大时,概率之间的差异会相对减少。记 m m m= ⌊ 2 k n ⌋ \lfloor \cfrac{2^k}{n}\rfloor n2k r r r= 2 k % n 2^k\% n 2k%n,则区间[0, r r r-1]内的每个 x 0 x_0 x0出现的概率 P ( x P(x P(x= x 0 ) = m + 1 2 k x_0)=\cfrac {m+1}{2^k} x0)=2km+1,区间[ r r r, n n n-1]内的每个 x 0 x_0 x0出现的概率 P ( x P(x P(x= x 0 ) = m 2 k x_0)=\cfrac {m}{2^k} x0)=2km,概率之比为 ( m + 1 ) : m (m+1):m (m+1):m。当 n n n取100000, k k k取30时,计算得到 m m m的值为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个整数
使用上述代码随机产生的20个浮点数如下图所示:
随机生成20个浮点数

随机序列的生成

如果是带有重复元素的随机序列,可以直接把 n n n个随机数拼接在一起,如果要去重的话就会稍微麻烦一些。

· 通过随机全排列生成随机序列

当随机序列的值域比较小的时候,这里假设元素的取值范围是[1, N N N],可以先产生 1 1 1~ N N N的随机排列,然后取出前 n n n项即可。

随机排列的生成可以使用STL中的random_shuffle()函数,也可以用以下方法生成:
(1) 初始化长度为 N N N的数组 a a a[ ],其中第 k k k个元素是 k + 1 k+1 k+1
(2) 接下来逐个选出随机序列的元素。假设当前正在进行第 i i i次循环,可以先在[0, N N N- i i i-1]的范围内产生一个随机数 x x x,令 j j j= x x x+ i i i,然后交换a[i]与a[j]。
(3) 重复上述过程,直到选出 n n n个元素。

对于元素x,被选出的概率 P ( x ) = p ∗ ∑ k = 1 n p × ( 1 − p ) k = p × [ 1 + ( 1 − p ) + ( 1 − p ) 2 + … + ( 1 − p ) n − 1 ] = p × [ 1 − ( 1 − p ) n ] / p = 1 − ( 1 − p ) n P(x)=p*\sum_{k=1}^n{p\times (1-p)^k}=p\times [1+(1-p)+(1-p)^2+…+(1-p)^{n-1}]=p\times [1-(1-p)^n]/p=1-(1-p)^n P(x)=pk=1np×(1p)k=p×[1+(1p)+(1p)2++(1p)n1]=p×[1(1p)n]/p=1(1p)n,其中 p = 1 N p=\frac{1}{N} p=N1。当 N = n N=n N=n时,每个元素一定会被选中,这种方法也可以用来随机打乱数组。
另外,上述过程的时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( N ) O(N) O(N)

以下是使用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集合进行去重:
每次产生一个随机数 x x x,然后加到set集合中去,如果元素 x x x已经在set中存在,就不会被添加。重复上述过程直到set中元素的个数恰好为 n n n

接下来分析这种方法的时间效率:
E ( k ) E(k) E(k)表示已经放进 k k k个元素时,剩余的期望操作步数。
首先可以得到 E ( n ) = 0 E(n)=0 E(n)=0,以及状态转移方程 E ( k ) = ( 1 − k N ) × E ( k + 1 ) + k N × E ( k ) + 1 E(k)=(1-\frac{k}{N})\times E(k+1)+\frac{k}{N}\times E(k)+1 E(k)=(1Nk)×E(k+1)+Nk×E(k)+1,进而得到递推关系 E ( k ) = E ( k + 1 ) + N N − k E(k)=E(k+1)+\frac{N}{N-k} E(k)=E(k+1)+NkN,由此推出: E ( 0 ) = ∑ k = 0 n − 1 N N − k ≤ N ∫ 0 n d x N − x = N ln ⁡ N N − n E(0)=\sum_{k=0}^{n-1}\cfrac{N}{N-k}\leq N \int _0^n \cfrac{dx}{N-x} =N\ln\cfrac{N}{N-n} E(0)=k=0n1NkNN0nNxdx=NlnNnN N = k × n N=k\times n N=k×n,化简得到 E ( 0 ) ≤ n ln ⁡ ( k k − 1 ) k E(0)\leq n\ln(\frac{k}{k-1})^k E(0)nln(k1k)k。当 k k k= 2 2 2时,期望步数的上限为 1.386 n 1.386 n 1.386n。当 k k k= 10 10 10时,期望步数的上限为 1.054 n 1.054n 1.054n。当 k k k趋近于无穷大时, ( k k − 1 ) k (\frac{k}{k-1})^k (k1k)k趋近于 e e e,期望步数会趋近于 n n n。因此期望时间复杂度近似为 O ( n log ⁡ n ) O(n\log n) O(nlogn)

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()函数重新打乱顺序。

  • 5
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值