随机数与随机序列生成

版权声明:版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/weixin_43692336/article/details/84564394

随机数的生成

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

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

随机数分布测试结果

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

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

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

用类似的方法,我们可以等概率的生成[0,2k-1]范围内的随机数 R R 。然后可以用 R R n n 取模得到一个[0,n-1]范围内的随机数 x x
虽然这样得到的 x x 仍然不是等概率的,但是当 k k 增大时,概率之间的差异会相对减少。记 m m = 2 k n \lfloor \cfrac{2^k}{n}\rfloor r r = 2 k % n 2^k\% n ,则区间[0, r r -1]内的每个 x 0 x_0 出现的概率 P ( x P(x = x 0 ) = m + 1 2 k x_0)=\cfrac {m+1}{2^k} ,区间[ r r , n n -1]内的每个 x 0 x_0 出现的概率 P ( x P(x = x 0 ) = m 2 k x_0)=\cfrac {m}{2^k} ,概率之比为 ( m + 1 ) : m (m+1):m 。当 n n 取100000, k k 取30时,计算得到 m m 的值为10737,概率之间的差异不到0.0001。

扫描二维码关注公众号,回复: 5535060 查看本文章

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 个随机数拼接在一起,如果要去重的话就会稍微麻烦一些。

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

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

随机排列的生成可以使用STL中的random_shuffle()函数,也可以用以下方法生成:
(1) 初始化长度为 N N 的数组 a a [ ],其中第 k k 个元素是 k + 1 k+1
(2) 接下来逐个选出随机序列的元素。假设当前正在进行第 i i 次循环,可以先在[0, N N - i i -1]的范围内产生一个随机数 x x ,令 j j = x x + i i ,然后交换a[i]与a[j]。
(3) 重复上述过程,直到选出 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 = 1 N p=\frac{1}{N} 。当 N = n N=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 ,然后加到set集合中去,如果元素 x x 已经在set中存在,就不会被添加。重复上述过程直到set中元素的个数恰好为 n n

接下来分析这种方法的时间效率:
E ( k ) E(k) 表示已经放进 k k 个元素时,剩余的期望操作步数。
首先可以得到 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 ) = E ( k + 1 ) + N N k E(k)=E(k+1)+\frac{N}{N-k} ,由此推出: 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} N = k × n N=k\times n ,化简得到 E ( 0 ) n ln ( k k 1 ) k E(0)\leq n\ln(\frac{k}{k-1})^k 。当 k k = 2 2 时,期望步数的上限为 1.386 n 1.386 n 。当 k k = 10 10 时,期望步数的上限为 1.054 n 1.054n 。当 k k 趋近于无穷大时, ( k k 1 ) k (\frac{k}{k-1})^k 趋近于 e e ,期望步数会趋近于 n n 。因此期望时间复杂度近似为 O ( n log n ) O(n\log n)

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

猜你喜欢

转载自blog.csdn.net/weixin_43692336/article/details/84564394