历史悠久的rand()
我们会使用从C继承而来的 int rand(); 函数作为随机数发生器,该随机数的范围为[0, RAND_MAX],其中 RAND_MAX 是 <stdlib.h> 中通过宏定义的一个常量,在C和C++标准中,均为“不低于32767的正整数”,大部分编译器都使用了32767作为 RAND_MAX。
如果我们要使用它的话,需要注意的是,这个rand虽然随机性较好,但一般的用法 rand() % RANGE 很容易造成不均匀的随机结果,例如当 #define RAND_MAX 32767 RANGE = 1000; 时,[0, 767]范围内的概率要比[768, 999]范围内的概率大,这是因为随机数的区间[0, 32767]中最后的那部分[32000, 32767],由于缺少了[32768, 32999],故 rand() % RANGE 落在[0, 767]的概率要更大。
解决方式也很简单,先将随机结果转化为浮点型,再按比例缩放为整形: int(((double)rand() / (RAND_MAX + 1)) * RANGE) ,这个方法可以解决上面的问题,但由于计算过程很可能造成精度损失,最终又会带来新的不平均。
为了不总是生成同样的随机序列,一般会使用 void srand(unsigned int seed); 初始化随机数发生器,如果我们要生成固定序列的随机数,使用同样的随机数种子就好,如果需要更加的随机一些,使用 <ctime> 中的 clock_t clock(); 是个不错的选择,因为它的返回值是系统时间的整型表达,精度为毫秒,能达到比较强的随机性。
C++11新增的随机函数
基于 int rand(); 的局限性考虑,C++11中新增了一堆随机数引擎、随机数分布器、随机数适配器,可根据指定的规则生成概率密度函数或离散概率分布,也就是下标为浮点数或整数的随机数。
-
分布器
里面有很多实用的随机数分布器,用于把原始随机数按照指定分布进行映射,来生成所需要的特定随机序列:
- uniform_int_distribution 均匀分布的随机整型(和上面的 int rand(); 功能类似)
- uniform_real_distribution 均匀分布的随机浮点型
- bernoulli_distribution 随机布尔型
- binomial_distribution 二项分布的随机整型
- binomial_distribution 几何分布的随机整型
- exponential_distribution 指数分布的随机整型
- ......
等等,都是统计方面常用的函数,同时这些分布器也均为模版,虽然整型和浮点型的模版不能互相通用,但整型可以使用包括从 short 到 unsigned long long 在内的所有原始整型,浮点型也可以使用包括 float 、 double 和 long double 等所有原始浮点型。具体完整的分布器列表和使用方法可以查看RandomNumberDistribution。
-
引擎
上面的这些分布器其实是将原始的随机数以定制好的分布情况映射而来的,而C++11也提供了很多种随机数生成引擎,如最常用的 default_random_engine ,其定义为 std::linear_congruential_engine<std::uint_fast32_t, 16807, 0, 2147483647> ,而 linear_congruential_engine 的数据结构模版为:
template< class UIntType, // 指定数据类型 UIntType a, UIntType c, UIntType m // 生成公式:x[i+1] = (a * x[i] + c) mod m > class linear_congruential_engine;
这个公式极其简单,而且随机性在某些情况下并不平均,所以像我这种强迫症可能并不满足,于是C++11也提供了很多其他的引擎可供选择,比如说采用了马特赛特旋转演算法(Mersenne Twister)的 mersenne_twister_engine :
template< class UIntType, size_t w, size_t n, size_t m, size_t r, // 参数含义未知,待补充 UIntType a, size_t u, UIntType d, size_t s, UIntType b, size_t t, UIntType c, size_t l, UIntType f > class mersenne_twister_engine;
一般使用 mt19937 ,也就是
std::mersenne_twister_engine<std::uint_fast32_t, 32, 624, 397, 31, 0x9908b0df, 11, 0xffffffff, 7, 0x9d2c5680, 15, 0xefc60000, 18, 1812433253>
该算法(使用上述参数时)获取的随机数,周期长达219937-1,也就是说,生成了219937-1个数后,才会开始生成一模一样的序列,且分布极其均匀,可算是最常用的随机数生成算法之一。
另外还有64位版本的 mt19937_64 :
std::mersenne_twister_engine<std::uint_fast64_t, 64, 312, 156, 31, 0xb5026f5aa96619e9, 29, 0x5555555555555555, 17, 0x71d67fffeda60000, 37, 0xfff7eee000000000, 43, 6364136223846793005>
这种算法算是基于有限二进制字段上的矩阵线性再生,可在生成高质量随机数的同时,保证很高的效率,一般来说可以满足绝大部分人的需要了。
下面是这个算法的伪代码(某呼上找的,来源未知):
另外还有一种滞后Fibonacci类型的subtract with carry(直译为带进位减法?)算法引擎 subtract_with_carry_engine :
template< class UIntType, size_t w, size_t s, size_t r // 需满足:0<s<r,0<w > class subtract_with_carry_engine;
下面是张来自Wikipedia的算法说明,细节就不做介绍了(因为我也不懂)。
where.
其中:
上述引擎都是使用一定的算法生成的伪随机数序列,而 random_device 则是生成非确定随机数的均匀分布整数随机数生成器,是使用计算机硬件来生成真随机序列(如果硬件不支持或不可用,或编译器不支持,则生成的依旧会是伪随机数序列)的引擎,用法与其他随机数引擎类似。
PS:Unix/Linux下的g++会通过调用 /dev/urandom 获取真随机序列,但Windows下的mingw、mingw-w64、VS都没实现真随机((‵□′))
-
引擎适配器
引擎适配器就是将其他引擎或引擎适配器生成的随机数作为输入,按照一定的规则,再变换生成一次,简单地说,就是随机数的二次生成,比如:
生成指定二进制位数的随机数的 independent_bits_engine :
template<
class Engine,
std::size_t W, // 表示指定的二进制位数,需满足:0<=W<=std::numeric_limits<UIntType>::digits(即UIntType的总位数)
class UIntType
> class independent_bits_engine;
会随机丢弃(可指定频率的)原生成序列中随机数的 discard_block_engine :
template< class Engine, size_t P, size_t R // 每P个Engine生成的随机数,随机保留R个,其余丢弃,需满足:0<R<=P > class discard_block_engine;
打乱原生成序列顺序的 shuffle_order_engine :
template< class Engine, std::size_t K // 维护一个K大小的表并预装入K个随机数,每次生成从表中随机取出一个数,并把Engine生成的随机数替换入表中 > class shuffle_order_engine;
需要特别注意的是, random_device 虽然也是一种随机数引擎,但其并不是 RandomNumberEngine ,也就是不能作为引擎适配器中的基础引擎。所以,如果无法实现真随机,那么这货就没用了……
简单的例子
下面是一段生成总大小为1GB的随机大数并二进制形式输出至文件的程序,使用了 std::mt19937_64 作为基础引擎, std::discard_block_engine<std::mt19937_64, 20, 15> 作为引擎适配器, std::uniform_int_distribution<unsigned> 作为分布器:
#include <fstream> #include <random> #include <iostream> const unsigned long long DATA_SIZE = 1073741824; // shoud be times of DATA_SINGLE_SIZE const unsigned long long DATA_SINGLE_SIZE = 128; // shoud be times of 8 const unsigned long long DATA_COUNT = DATA_SIZE / DATA_SINGLE_SIZE; typedef unsigned number_type; struct Data { number_type v[DATA_SINGLE_SIZE / sizeof(number_type)]; }; int main() { std::ofstream outfile; outfile.open("text_data.dat", std::ios_base::binary); if (outfile.is_open()){ std::discard_block_engine<std::mt19937_64, 20, 15> generator_random_discard; std::uniform_int_distribution<number_type> distribution; Data temp; outfile.write((char*)&DATA_SIZE, sizeof(DATA_SIZE)); outfile.write((char*)&DATA_SINGLE_SIZE, sizeof(DATA_SINGLE_SIZE)); for (int i = 0; i < DATA_COUNT; i++) { for (auto v = std::begin(temp.v); v != std::end(temp.v); v++) { *v = distribution(generator_random_discard); } outfile.write((char*)&temp, sizeof(Data)); } outfile.close(); } else { std::cerr << "Can\'t create the file [test_data.dat]!" << std::endl; return EXIT_FAILURE; } return EXIT_SUCCESS; }
补充
时间函数time()和clock()也有了C++11的版本,具体使用方法下次再细说,下面是一个在stackoverflow.com上的代码:
#include <chrono> #include <random> #include <iostream> int main() { std::mt19937 eng(std::chrono::high_resolution_clock::now() .time_since_epoch().count()); std::uniform_real_distribution<> unif; std::cout << unif(eng) << '\n'; }