随机数的生成
在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个浮点数如下图所示:
随机序列的生成
如果是带有重复元素的随机序列,可以直接把 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)=p∗∑k=1np×(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
=
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)=(1−Nk)×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)+N−kN,由此推出:
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=0∑n−1N−kN≤N∫0nN−xdx=NlnN−nN令
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(k−1k)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
(k−1k)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()函数重新打乱顺序。