1. 題目
已知一個能產生 [0, n) 的隨機數的函數,設計一個能產生 [0, m)的隨機數的函數。
要產生 [0, m) 的隨機數,首先要確保輸出 0、1、2、…、m-1 的概率相同。
2. 驗證函數 RandN
其中,RandN 表示能產生 [0, n) 的隨機數的函數,如下所示:
unsigned int RandN(unsigned int N)
{
return 0 == N ? 0 : rand() % N;
}
此處令 N = 10,進行 1 000 000 次重複實驗,統計 0,1,2,3,4,5,6,7,8,9出現的頻度。如下圖所示:
0,1,2,3,4,5,6,7,8,9出現的頻度近似相等,可以理解爲等概率分佈。
3. 設計函數 RandM M 小於等於 N
如果 M = N 很簡單,就是同一個函數;
如果 M < N,可以考慮使用截斷的方式,因爲 RandN 產生的 [0, 1, 2, 3, …, N-1] 是等概率的,隨機的,因此 [0, 1, 2, 3, …, M-1] 也就等概率的,所以最簡單的方式就是:如果 RandN() 輸出小於 M,則直接作爲 RandM() 的輸出,否則繼續生成,直到小於 M。
上述方法又一個欠缺,如果 N 較大,比如 N =1000,M較小,M = 10,那麼從統計的角度考慮,要執行 100 次 RandN() 才能成功輸出一個 RandM(),也就是所成功的概率是:
針對上述方案的解決方案如下:
取一個整數 K,令
代碼如下:
unsigned int RandmUsingRandn(unsigned int M, unsigned int N, unsigned int(*func)(unsigned int))
{
if (0 == M || 0 == N)
{
return 0;
}
if (M <= N)
{
// 取小於 N 的 M 的最大倍數,提高效率
int iNDivM = N / M;
int iTimesM = iNDivM * M;
int iRandN = func(N); // 截尾能保證前面的概率依然相同
while (iRandN >= iTimesM)
{
iRandN = func(N);
}
return iRandN % M;
}
}
例如:令 N = 27,M=13,K =26,把 K 作爲截尾閾值,進行 1 300 000 次重複實驗,統計 0,1,2,3,4,5,6,7,8,9,10,11,12 出現的頻度。如下圖所示:
0,1,2,3,4,5,6,7,8,9,10,11,12 出現的頻度近似相等,可以理解爲等概率分佈。
4. 設計函數 RandM M大於等於N
由於 M > N,所以不能使用簡單的截斷方法了,但是如果能產生一個大於 M 的隨機數,那麼就可以借用上述 M < N的情況。很幸運的是,有 RandN(N) 可以很方便的設計 RandN2(N*N)。
令
令
則,
上述式子的含義就是任意從 N1中取一個數,任意從 N2 中取一個數,他們的和不會有重複,正好等於
所以下面的函數就可以很簡單的生成一個 [0, N*N)之間的隨機數:
int iRandN2 = RandN(N) * N + RandN(N);
此時,就可以借用 M < N 的情況,有如下代碼:
unsigned int RandmUsingRandn(unsigned int M, unsigned int N, unsigned int(*func)(unsigned int))
{
if (0 == M || 0 == N)
{
return 0;
}
if (M > N)
{
// 取小於 N^2 的 M 的最大倍數,提高效率
int iN2DivM = N * N / M;
int iTimesM = iN2DivM * M;
// 有 Rand(N * N) = Rand(N) * N + Rand(N)
// 同理可以推出 Rand(N^3), Rand(N^4)
int iRandN2 = func(N) * N + func(N); // 截尾能保證前面的概率依然相同
while (iRandN2 >= iTimesM)
{
iRandN2 = func(N) * N + func(N);
}
return iRandN2 % M;
}
}
例如:令 N = 7,M=15,K = 45,把 K = 45 作爲截尾閾值,進行 1 500 000 次重複實驗,統計 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14 出現的頻度。如下圖所示:
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14 出現的頻度近似相等,可以理解爲等概率分佈。
注意:上述部份仍然有問題,因爲我們人爲假設
N2≥M ,如果N2≤M 呢,此時就需要產生更大的隨機數了。
由 RandN(N) 可以很簡單的推出 RandN2(N*N),同樣,也可以推出 RandN3(N*N*N)等等。
令
令
令
令
上述推導其實可以看成一個N進制的數字:
1.
2.
3.
所以
根據上述規律,完全可以由 RandN() 來產生 [0, n),[0, n^2), [0, n^3), [0, n^4), [0, n^5),。。。
從上面的分析似乎我們總可以找到一個能產生大於 M 的隨機數,然而,我們沒有考慮另外一種情況,如果M 和 N 都較大時,N^2或者N^3就有可能超過整數的範圍現在,此時,可能需要需要使用
unsigned long long int
作爲中間變量了。
5. 參考
《程序員面試筆試寶典(2014版)》7.15.12