在 C++ 中生成随机数字
Yi Fan

rand()srand()

<cstdlib> 中提供的 rand() 是比较常用的生成随机数的方法。它可以通过线性同余法提供一个 [0,RAND_MAX][0, RAND\_MAX] 中的伪随机数,类型为 int

1
#define RAND_MAX 0x7fff

根据定义,RAND_MAX 大小为 21612^{16}-1 ,也就是说 rand() 生成的数字将会落在 [0,2161][0, 2^{16}-1] 这个区间内。

rand() 本身规则是以 1 为种子,如果在同一个程序中多次运行 rand(),可以得到不同的结果。不过,下一次运行程序时将会重复一模一样的序列,某种程度上,这样的随机数失去了意义。

解决方法一般是用 srand()rand() 进行初始化。srand() 接受一个无符号整型作为输入,这个值将会作为种子供 rand() 进行迭代。srand() 可以多次使用,每次使用都会让 rand() 使用新种子开启新一轮迭代。

显然,如果 srand() 每次都接收到一个相同的值,那么这个初始化也就没有意义了。因此,一般使用一些会流变的特殊值进行初始化,其中最为方便的就是时间。

<ctime> 中提供了 time(),它可以获取自 1970 年 1 月 1 日以来经过的秒数,类型为 time_t,是一个整型。这个函数可以返回一个值,允许直接把返回值赋给 int 类型。同时,函数也允许提供一个类型为 time_t 的指针作为参数,返回值会写入指针地址。这个参数允许空指针。需要注意的是,由于 srand() 接受的是一个无符号整型,所以在使用时应该进行类型转换。

另一种方法借助于 <windows.h> 中提供的 GetTickCount() 函数,它返回自系统启动以来经过的毫秒数,类型为 DWORD。该类型本身就是一个无符号整型,可以直接传递给 srand()。这种方法的好处是精度更高(毫秒级),可以一定程度上避免并行时获取到相同种子。

1
2
3
// 使用例
srand((unsigned)time(NULL));
srand(GetTickCount());

<random>

从 C++11 开始,<random> 被引入。这个库允许定义随机数生成工具,并且允许对随机数的分布进行定义。

均匀随机数生成器

<random> 库中定义了一些均匀随机数生成器(Uniform Random Number Generator, URNG),他们被称为引擎。这些引擎可以生成范围内的均匀随机数。比较常见的有以下几个:

  • mt19937:应用梅森旋转算法,32 位字长。
  • mt19937_64:同样是梅森旋转算法,不过字长为 64 位。
  • knuth_b:应用高德纳随机算法。
  • minstd_rand:应用线性同余法。
  • ranlux24:应用 Ranlux 算法,生成字长为 24 位的随机数。
  • ranlux24_baseranlux24 的基础引擎。
  • ranlux48/ranlux48_base:同上,但生成字长为 48 位的随机数。
  • default_random_engine:默认引擎,不过不清楚实际调用了哪个引擎。(不推荐使用)

以上这些都可以在范围内生成均匀的随机数。在使用时可以按照以下方法:

1
2
mt19937 E((unsigned)time(NULL)); // 定义引擎类型,此时可以传入种子
std::cout << E() << std::endl; // 调用引擎生成随机数

分布类型

<random> 还允许对分布类型进行定义。常见的分布类型有:

  1. 均匀分布
    • uniform_int_distribution:给定闭区间内生成整数的均匀分布。
    • uniform_real_distribution:给定左闭右开区间内生成浮点数的均匀分布。
    • generate_canonical:生成 [0,1)[0, 1) 内均匀的浮点数分布,可以指定精度。
  2. 伯努利分布
    • bernoulli_distribution:生成布尔值的伯努利分布。
    • binomial_distribution:生成整数值的二项式分布。
    • geometric_distribution:生成整数值的几何分布。
    • negative_binomial_distribution:生成整数值的负二项式分布。
  3. 正态分布
    • cauchy_distribution:生成浮点值的柯西分布。
    • chi_squared_distribution:生成浮点值的卡方分布。
    • fisher_f_distribution:生成浮点值的 F 分布(Fisher-Snedecor 分布)。
    • lognormal_distribution:生成浮点值的对数正态分布。
    • normal_distribution:生成浮点值的正态分布(高斯分布)。
    • student_t_distribution:生成浮点值的学生 t 分布。
  4. 泊松分布
    • exponential_distribution:生成浮点值的指数分布。
    • extreme_value_distribution:生成浮点值的极值分布。
    • gamma_distribution:生成浮点值的伽马分布。
    • poisson_distribution:生成整数值的泊松分布。
    • weibull_distribution:生成浮点值的韦伯分布。
  5. 抽样分布
    • discrete_distribution:生成离散整数分布。
    • piecewise_constant_distribution:生成浮点值的分段常数分布。
    • piecewise_linear_distribution:生成浮点值的分段线性分布。

不同的分布方法允许不同的传入参数,举例来说,normal_distribution 允许传入两个参数,前者为期望,后者为标准差。

1
2
3
mt19937 e;
normal_distribution <double> dis(0.0, 1.0);// 期望为 0.0,标准差为 1.0 的正态分布
std::cout << dis(e) << std::endl; // 获取随机数时把引擎传入

需要注意的是,尖括号内虽然允许传递模板参数,但不能传递不同的数据类型。举例来说,uniform_int_distribution 后允许传入 shortintlong long 等整型,但不允许传入 floatdouble 等浮点型。

random_device

random_device 也是一种 URNG,但是不同之处是,random_device 可以获取真随机数。真随机数可能来源于 Linux 维护的熵池,在 Windows 上可能来源于微软的加密 API,在没有获取真随机数的途径时,它会生成伪随机数。

这种方法的优点就在于其真随机性,但是弊端也很明显:较慢的获取速度,同时熵池是有限的,在无法获取真随机数时,random_device 改为生成伪随机数。这表明 random_device 是不稳定的,并不适合直接作为随机数生成器调用多次。

一般来说,random_device 的用法是代替时间函数作为种子。用 random_device 生成一个真随机数,然后把这个随机数作为种子传递给其他伪随机数生成器。

1
2
3
4
5
// 使用例
random_device seed;
mt19937 e(seed());
uniform_int_distribution <int> dis(10, 20);
std::cout << dis(e) << std::endl;

总结

一般情况下,rand() 配合时间函数作为种子,可以生成比较令人满意的随机数。在有特殊的对于随机数分布的要求或者是对于随机浮点数的需求时,可以调用 <random> 来实现。