標籤:

C++的RAND函數生成的值為什麼存在嚴重的不隨機性?

用到0~10000的隨機,在C++中使用了rand()%10000,結果測試了100億次數據後,發現在2767值處出現斷層,前面0~2767都是122萬次左右,後面的都只有91.5萬次左右。。。這個是怎麼回事,求助~~


謝邀。

先說結論:這裡出現不均勻的現象不是因為rand本身的問題,而是因為使用的方法不對。

首先,rand的返回值範圍為 [0, RAND_MAX]。 在Windows環境下,提供的 RAND_MAX 一般為32767。也就是說,對於 rand() % 10000 來說,如果取到 [0, 2767] 這個範圍的值,那麼有四種情況:0xxxx、1xxxx、2xxxx、3xxxx,概率為 frac{4}{32768} ;而如果取到 [2768, 9999] 這個範圍的值,就只有三種情況:0xxxx、1xxxx、2xxxx,概率為 frac{3}{32768}

這樣問題就迎刃而解了:

1000000 	imes frac{4}{32768} approx 122.07

以及

1000000 	imes frac{3}{32768} approx 91.55

這也是題主測試所得到的數字。

解決方法:(轉成浮點數的方法不對,這條劃掉),或者改用C++11的偽隨機數生成器(推薦),或者乾脆自己寫個偽隨機數生成器。

話說,看到2767應該能想到32767吧。


rand() Considered Harmful

用浮點數也不能保證均勻分布。


2767 = 32767 % 10000


確實 rand 返回的數字範圍太小了。

讓我們用中學數學裡常用的一種思維,就是考慮一種極端案例,假設我們恰好一共調用32767次,rand 依次返回 0到32767 各一次,然後看看這些數字對 10000 取余的結果,設我們有一萬個桶排成一行,我們從頭走到尾,每個桶里投一個數字,走到行尾部10000以後進行了折回 ( mod 導致的),回到行首走第二趟。因此,前 2767 個桶里有4個數字。2767 以後的桶里有3個數字。因為數字在 32767 之後沒有了,在最後一趟時走到行中間就結束了,沒能覆蓋整行。

說 minvalue + (int)((maxⅴalue - minvalue ) * rand() / 32767.0) 這個如果要的數字範圍很大,會不能全覆蓋。

本質原因是進位不同(10000 不是 2 的整次方)引起的。換句話說,如果題主要是對 256,512,1024,取模,就可以很容易的做到均勻分布。就好比假如說我是賣甘蔗的,我有15,16根甘蔗(bits),你要幾根(bits)我給你幾根(bit)是最好的,如果你非要5根加 1 / 3 根(bits),那我賣不了,這根線不能從中間劈開。

因為 rand 分布範圍是 2 的 k 次方範圍,是用二進位組合起來的,題主要的是10進位上的範圍,所以對題主預期意圖實際上沒有數學上完美的解決辦法,只能儘可能接近均勻全覆蓋。我們一共有 32767 個數字,如果減少桶的個數,桶中的數字個數就會越發接近,差異越無關緊要。因此下面通過分段組合來近似均勻全覆蓋,比如:

(1) rand % 100 * 100

+ rand % 100;

(2) rand % 10 * 1000

+ rand % 10 * 100

+ rand % 10 * 10

+ rand % 10;

和以上的同理,你會發現實際上上面的表達式中每個 mod 都分布不均勻,但後者的那一行桶的行粒度更小,所以針對海量取樣的不均勻性就會更小,因此組合到一起以後, 後者比 前者覆蓋的更均勻。


tl:dr:所有rand / x*y都越改越錯,五十步笑百步。

---

我的天哪,這都几几年了還在閉門造車?隨機數取模還說得過去,畢竟最廣為流傳。但建議cast到float再cast回來,或者開不知道哪來沒驗證的私車的人,全是越改越錯。

難道就不知道用用標準&的東西?

std::default_random_engine generator;

//需要nondeterministic/true random的用std::random_device,可抗密碼學攻擊,除非是30年前的老爺機

std::uniform_int_distribution& distribution(28,274);

long sample = distribution(generator);

&里的分布希么都有: uniform_real_distribution, bernoulli_distribution, binomial_distribution, geometric_distribution, negative_binomial_distribution, poisson_distribution, exponential_distribution, gamma_distribution, weibull_distribution, extreme_value_distribution, normal_distribution, lognormal_distribution, chi_squared_distribution, cauchy_distribution, fisher_f_distribution, student_t_distribution, discrete_distribution, piecewise_constant_distribution, piecewise_linear_distribution

你這輩子都沒見過也不會見到的distribution全齊了,而且都是經得起密碼學攻擊的實現

pseudorandom generator有minstd_rand, minstd_rand0, minstd_rand0, mt19937, mt19937_64, ranlux24_base, ranlux48_base, ranlux24, ranlux48, knuth_b

一樣也都是你沒見過也不會見到的pseudorandom演算法全齊了,都是經過時間和經驗檢驗的演算法和標準實現。


給你分析一下:

rand() 產生的數應該是在0~32767 均勻分布的,所以如果拿它mod 10000的話,

則相當於把編號0到32767的球按照他們對於10000的餘數放到10000個箱子里去, 於是乎, 前2767個盒子放了4個球,後面的盒子只放了3個球,於是乎,最後數的其實是這10000個盒子里,每個盒子所得到的球數在32767次放球操作中的比例, 4/32767= 0.00012207, 3/32767=0.00009156, 正好與你的實驗結果相吻合,證明了說 rand函數在0~32767之間是均勻分布的。

然後你想產生0-10000的隨機數,

正確的做法是 rand()/32767.0 * 10000.


用 @lichray 大大的 randint(http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4531.html)

std::experimental::randint(0, 10000);


你都求模取余了,原來再均勻都被這樣搞成不均勻了。


好像有研究說,生成指定[minValue,maxValue]範圍的隨機數不能用%,需要這麼寫:

int value=minValue+int((maxValue-minValue)*(rand()/(float)RAND_MAX));

其中RAND_MAX是標準庫內置的一個宏,表示rand()函數可能生成的最大的隨機數,rand()/(float)RAND_MAX表示把rand()的值均勻縮放到[0,1]範圍內,再把這個[0,1]範圍內的值縮放到需要的範圍。

你可以試一下這個辦法。


for (;;){int x = rand(); if (x &< 3*10000) return x%1000}這樣就可以吧?


關於這個東西。。其實前面的答主給出的解決方案似乎都不太嚴謹。。其實這個問題沒那麼容易完美解決,最好的做法就是用輸出大小大點的prng..或者csprng..這些東西通過值域大或難預測性能一定程度上保證質量。。還有mt19937其實輸出大小可以看成是長度大於1K的二進位序列所以用哪個迭代的還是可接受的。。


1. 不要用取模,用按比例縮放後取整。

2. 不要用 rand(),改成讀 /dev/urandom,要求高的話(比如用於加密演算法)用 /dev/random。


如果非要用取模運算,千萬不要用10或者2的n次方


不查msdn的嗎?有例子啊。返回range_min&<=random number&int u=(double)rand()/(RAND_MAX+1)*(range_max-range_min)+range_min


因為rand的生成值範圍在0-32767。


@SuperSodaSea 分析蠻合理的。

按照他的思路算了一下:

假設 RAND_MAX = 32767 , 欲生成 0~9999 範圍內隨機數。

直接 rand() % 10000 是不行,因為 rand() 的範圍是 0 ~ 32767 。

使用取模的方法,為了能夠獲取均勻的隨機值,必須使被取模的數是10000的整數倍。

假設這個數是 0~29999,那麼取模後的分布是均勻的。

試著寫了一下:

int get_rand(int n)
{
int rand_result;
int rand_max = RAND_MAX / n * n;
do {
rand_result = rand();
} while (rand_result &>= rand_max);
return rand_result % n;
}

P.S. C++11 出了&,像這種公用的東西盡量拿現成的。。


int32 makeUniformRand(int32 fr, int32 to)

{

if (fr &> to) {

int32 tmp = to;

to = fr;

fr = tmp;

}

int32 num = (to - fr + 1);

int32 thisRandMax = (RAND_MAX/num)*num-1;

int32 r = 0;

do {

r = rand();

} while (r &> thisRandMax);

return fr + r%num;

}


主要你用mod了吧………

我上次也遇到了……於是乎用(mod * mod * mod )mod這麼取隨機………


不能直接取模,得到的結果不均勻。一個簡單的演算法是,如果rand()的結果大於等於10000,則重新rand(),直到rand()結果小於10000


這是因為通常C/C++自帶的那個PRNG是個玩具級別的東西。干正事都不應該用。應該用更好的演算法。


推薦閱讀:

在c語言中,使用函數指針是否可以提高函數的調用速度 ?
C++中如何定義指向函數指針的指針?
如何有效的練習並且提升寫代碼的能力?

TAG:CC | 隨機數 |