歷史悠久的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'; }