大家在写 C/C++ 程序时,难免会遇到要求获取某个范围内的随机数,我查阅了一些资料后,总结如下。本文分两部分,先介绍 C 语言中与随机数相关的两个函数 srand 和 rand,后介绍 C++ 中的 random 库,每一部分最后会给出生成特定范围内的随机数模板供参考。
1 C 语言中的 srand 和 rand
1.1 实现
- #define RAND_MAX 32767 // in <stdlib.h>
- unsigned long _Randseed = 1; // global seed
- void srand(unsigned int seed) {
- _Randseed = seed;
- }
- int rand(void) {
- _Randseed = _Randseed * 1103515245 + 12345;
- return ((unsigned int)(_Randseed >> 16) & RAND_MAX);
- }
#define RAND_MAX 32767 // in <stdlib.h>
unsigned long _Randseed = 1; // global seed
void srand(unsigned int seed) {
_Randseed = seed;
}
int rand(void) {
_Randseed = _Randseed * 1103515245 + 12345;
return ((unsigned int)(_Randseed >> 16) & RAND_MAX);
}
第一次接触 C 语言中的随机数时,很疑惑为什么有种子这个玩意,只提供一个产生随机数的函数不就行了吗,看了上面的源码后,就明白了,因为计算机不能产生真正的随机数,只能靠数学的方法产生伪随机数。srand 函数的作用是设置种子,如果不设置的话种子(上面的 _Randseed)则默认初始是1,种子是全局变量。rand 的实现就跟数论有关了,上面的实现用的是线性同余法。可以看到它的返回值范围是 [0, RAND_MAX]。
1.2 time
既然计算机不能产生真正的随机数,那怎么才能使程序每次运行的结果不同呢?总得有个随机的东西,那就借助 time 这个函数产生种子,引入一个新东西又会带来一些坑,我早年写过这种程序:
- // 生成十个随机数(错误用法)
- for (int i = 0; i < 10; ++i) {
- srand((unsigned int)time(NULL));
- printf("%d: %d\n", i, rand());
- }
// 生成十个随机数(错误用法)
for (int i = 0; i < 10; ++i) {
srand((unsigned int)time(NULL));
printf("%d: %d\n", i, rand());
}
它将产生10个相同的数。要解释这个问题,就得弄懂 time 这个函数,它的函数原型如下:
- time_t time(time_t *timer);
time_t time(time_t *timer);
它返回“当前时间”,这个“时间“的类型是 time_t,在 VC 中被 typedef 为 unsigned long,标准中只规定它是个算数类型,至于它是如何表示时间的未定义。一般是返回 UNIX 时间戳,定义为从格林威治时间1970年01月01日00时00分00秒起至现在的总秒数。上面的程序执行时很快,在一秒内完成循环,所以它产生了相同的随机数。
1.3 my rand
下面提供两个生成随机数的模板。
- int g_is_first = 1;
- /*
- ** return a random integer in the interval
- ** [a, b]
- */
- int uniform_int(int a, int b) {
- if (g_is_first) {
- g_is_first = 0;
- srand((unsigned int)time(NULL));
- }
- return (int)((double)rand() / ((RAND_MAX + 1.0) / (b - a + 1.0)) + a);
- }
- /*
- ** return a random real in the interval
- ** [a, b] (also [a, b))
- */
- double uniform_real(double a, double b) {
- if (g_is_first) {
- g_is_first = 0;
- srand((unsigned int)time(NULL));
- }
- return (double)rand() / ((double)RAND_MAX / (b - a)) + a;
- }
int g_is_first = 1;
/*
** return a random integer in the interval
** [a, b]
*/
int uniform_int(int a, int b) {
if (g_is_first) {
g_is_first = 0;
srand((unsigned int)time(NULL));
}
return (int)((double)rand() / ((RAND_MAX + 1.0) / (b - a + 1.0)) + a);
}
/*
** return a random real in the interval
** [a, b] (also [a, b))
*/
double uniform_real(double a, double b) {
if (g_is_first) {
g_is_first = 0;
srand((unsigned int)time(NULL));
}
return (double)rand() / ((double)RAND_MAX / (b - a)) + a;
}
为了保证 srand 函数只执行一次,这里用了全局标志 g_is_first。其实最好在头文件中定义接口,在源文件中实现,这里为了使用方便就全放一起了。当要求的随机数范围过大时,uniform_int 和 uniform_real 貌似有 bug。
2 C++ 中的 random 库
- ** return a random integer in the interval [a, b]
- */
- int uniform_int(int a, int b) {
- static std::default_random_engine e{std::random_device{}()}; // avoid "Most vexing parse"
- static std::uniform_int_distribution<int> u;
- return u(e, decltype(u)::param_type(a, b));
- }
- /*
- ** return a random real in the interval [a, b] (also [a, b))
- */
- double uniform_real(double a, double b) {
- static std::default_random_engine e{std::random_device{}()};
- static std::uniform_real_distribution<double> u;
- return u(e, decltype(u)::param_type(a, b));
- }
|
|
2. 使用方法
|
|
|
|
3. 注意事项
- 求一定范围内的随机数。
|
- 伪随机浮点数。
大家可能很多次讨论过随机数在计算机中怎样产生的问题,在这篇文章中,我会对这个问题进行更深入的探讨,阐述我对这个问题的理解。
|
|
|
3.建议:如果想在一个程序中生成随机数序列,需要至多在生成随机数之前设置一次随机种子。
|
用rand()和srand()产生伪随机数的方法总结
---------------------------------
标准库<cstdlib>(被包含于<iostream>中)提供两个帮助生成伪随机数的函数:
函数一:int rand(void);
从srand (seed)中指定的seed开始,返回一个[seed, RAND_MAX(0x7fff))间的随机整数。
函数二:void srand(unsigned seed);
参数seed是rand()的种子,用来初始化rand()的起始值。
可以认为rand()在每次被调用的时候,它会查看:
1) 如果用户在此之前调用过srand(seed),给seed指定了一个值,那么它会自动调用
srand(seed)一次来初始化它的起始值。
2) 如果用户在此之前没有调用过srand(seed),它会自动调用srand(1)一次。
根据上面的第一点我们可以得出:
1)如果希望rand()在每次程序运行时产生的值都不一样,必须给srand(seed)中的seed一个变值,这个变值必须在每次程序运行时都不一样(比如到目前为止流逝的时间)。
2) 否则,如果给seed指定的是一个定值,那么每次程序运行时rand()产生的值都会一样,虽然这个值会是[seed, RAND_MAX(0x7fff))之间的一个随机取得的值。
3)如果在调用rand()之前没有调用过srand(seed),效果将和调用了srand(1)再调用rand()一样(1也是一个定值)。
举几个例子,假设我们要取得0~6之间的随机整数(不含6本身):
例一,不指定seed:
for(int i=0;i<10;i++){
ran_num=rand() % 6;
cout<<ran_num<<" ";
}
每次运行都将输出:5 5 4 4 5 4 0 0 4 2
例二,指定seed为定值1:
srand(1);
for(int i=0;i<10;i++){
ran_num=rand() % 6;
cout<<ran_num<<" ";
}
每次运行都将输出:5 5 4 4 5 4 0 0 4 2
跟例子一的结果完全一样。
例三,指定seed为定值6:
srand(6);
for(int i=0;i<10;i++){
ran_num=rand() % 6;
cout<<ran_num<<" ";
}
每次运行都将输出:4 1 5 1 4 3 4 4 2 2
随机值也是在[0,6)之间,随得的值跟srand(1)不同,但是每次运行的结果都相同。
例四,指定seed为当前系统流逝了的时间(单位为秒):time_t time(0):
#include <ctime>
//…
srand((unsigned)time(0));
for(int i=0;i<10;i++){
ran_num=rand() % 6;
cout<<ran_num<<" ";
}
第一次运行时输出:0 1 5 4 5 0 2 3 4 2
第二次:3 2 3 0 3 5 5 2 2 3
总之,每次运行结果将不一样,因为每次启动程序的时刻都不同(间隔须大于1秒?,见下)。
关于time_t time(0):
time_t被定义为长整型,它返回从1970年1月1日零时零分零秒到目前为止所经过的时间,单位为秒。比如假设输出:
cout<<time(0);
值约为1169174701,约等于37(年)乘365(天)乘24(小时)乘3600(秒)(月日没算)。
另外,关于ran_num = rand() % 6,
将rand()的返回值与6求模是必须的,这样才能确保目的随机数落在[0,6)之间,否则rand()的返回值本身可能是很巨大的。
一个通用的公式是:
要取得[a,b)之间的随机整数,使用(rand() % (b-a))+ a (结果值将含a不含b)。
在a为0的情况下,简写为rand() % b。
最后,关于伪随机浮点数:
用rand() / double(RAND_MAX)可以取得0~1之间的浮点数(注意,不同于整型时候的公式,是除以,不是求模),举例:
double ran_numf=0.0;
srand((unsigned)time(0));
for(int i=0;i<10;i++){
ran_numf = rand() / (double)(RAND_MAX);
cout<<ran_numf<<" ";
}
运行结果为:0.716636,0.457725,…等10个0~1之间的浮点数,每次结果都不同。
如果想取更大范围的随机浮点数,比如1~10,可以将
rand() /(double)(RAND_MAX) 改为 rand() /(double)(RAND_MAX/10)
运行结果为:7.19362,6.45775,…等10个1~10之间的浮点数,每次结果都不同。
至于100,1000的情况,如此类推。
以上不是伪随机浮点数最好的实现方法,不过可以将就着用用…
问题1: 怎样获得一个真正的随机数?要知道,rand()是不能产生真正的随机数的!即使不能产生真正的随机数,也要大概接近呀!而rand()好象每次的随机都一样。
专家解答:
问题2: 我按照上述方法并不能产生随机数,仅产生公差为3或4的等差数列:
专家解答:
C语言生成随机数需要用到两个函数,一个是srand(),一个是rand(),首先给srand()提供一个种子,unsigned int类型,其取值范围从0~65535,srand()根据这个种子会由一个特定的公式生成一个随机数序列;然后调用rand(),它会依次从这个序列中返回一个数(在0到32767之间),而在不指定srand()种子的情况下,它每次都使用默认的种子,因此生成的序列是同一个,你每次运行,当然就取到相同的数字了。
你可以在程序里添加头文件time.h,用当前时间作为srand的种子,这样就能保证每次运行时都能取到不同的随机数序列。如下:
time_t t;
srand((unsigned) time(&t));
然后就可以用rand()取随机数了。