什么是隨機數?通俗說法就是隨機產生的一個數,這個數預先不能計算出來的,並且所有可能出現的數字,概率應該是均勻的。因此隨機數應該滿足至少以下兩點:
- 不可計算性,即不確定性。
- 機會均等,即每個可能出現的數字必須概率相等。
如何產生隨機數是一個具有挑戰的問題,一般使用隨機硬件產生,比如骰子、電子元件噪聲、核裂變等。
在計算機編程中,我們經常調用隨機數產生器函數,但我們必須清楚的一點是,一般直接調用軟件的隨機數產生器函數,產生的數字並不是嚴格的隨機數,而是通過一定的算法計算出來的(不滿足隨機數的不可計算性),我們稱它為偽隨機數!
由於它具有類似隨機的統計特征,在不是很嚴格的情況,使用軟件方式產生偽隨機相比硬件實現方式,成本更低並且操作簡單、效率也更高!
那一般偽隨機數如何產生呢? 一般是通過一個隨機種子(比如當前系統時間值),通過某個算法(一般是位運算),不斷迭代產生下一個數。比如c語言中的stdlib中rand_r
函數(用的glibc):
/* This algorithm is mentioned in the ISO C standard, here extended
for 32 bits. */
int
rand_r (unsigned int *seed)
{
unsigned int next = *seed;
int result;
next *= 1103515245;
next += 12345;
result = (unsigned int) (next / 65536) % 2048;
next *= 1103515245;
next += 12345;
result <<= 10;
result ^= (unsigned int) (next / 65536) % 1024;
next *= 1103515245;
next += 12345;
result <<= 10;
result ^= (unsigned int) (next / 65536) % 1024;
*seed = next;
return result;
}
而java中的Random類產生方法next()為:
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}
java中還有一個更精確的偽隨機產生器java.security.SecurityRandom, 它繼承自Random類,可以指定算法名稱,next方法為:
final protected int next(int numBits) {
int numBytes = (numBits+7)/8;
byte b[] = new byte[numBytes];
int next = 0;
nextBytes(b);
for (int i = 0; i < numBytes; i++) {
next = (next << 8) + (b[i] & 0xFF);
}
return next >>> (numBytes*8 - numBits);
}
當然這個類不僅僅是重寫了next方法,在種子設置等都進行了重寫。
最近有一道題:已知一個rand7函數,能夠產生1~7的隨機數,求一個函數,使其能夠產生1~10的隨機數。
顯然調用一次不可能滿足,必須多次調用!利用乘法原理,調用rand7() * rand7()可以產生149的隨機數,我們可以把結果模10(即取個位數)得到09的數,再加1,即產生110的數。但我們還需要保證概率的機會均等性。顯然1~49中,共有49個數,個位為0出現的次數要少1,不滿足概率均等,如果直接這樣計算,210出現的概率要比1出現的概率大!我們可以丟掉一些數字,比如不要大於40的數字,出現大於40,就重新產生。
int rand10() {
int ans;
do {
int i = rand7();
int j = rand7();
ans = i * j;
} while(ans > 40);
return ans % 10 + 1;
}
隨機數的用途就不用多說了,比如取樣,產生隨機密碼等。下面則着重說說其中一個應用--洗牌算法。
我們可能接觸比較多的一種情況是需要把一個無序的列表排序成一個有序列表。洗牌算法(shuffle)則是一個相反的過程,即把一個有序的列表(當然無序也無所謂)變成一個無序的列表。
這個新列表必須是隨機的,即原來的某個數在新列表的位置具有隨機性!
我們假設有1~100共100個無重復數字。
很容易想到一種方案是:
-
從第一張牌開始,利用隨機函數生成器產生1~100的隨機數,比如產生88,則看第88個位置有沒有占用,如果沒有占用則把當前牌放到第88位置,如果已經占用,則重新產生隨機數,直到找到有空位置!
首先必須承認這個方法是可以實現洗牌算法的。關鍵在於效率,首先空間復雜度是O(n),時間復雜度也是O(n),關鍵是越到后面越難找到空位置,大量時間浪費在求隨機數和找空位置的。
第二中方案:
- 從第一張牌開始,設當前位置牌為第i張,利用隨機函數生成器產生1~100的隨機數,比如產生88,則交換第i張牌和第88張牌。
這樣滿足了空間是O(1)的原地操作,時間復雜度是O(n)。但是否能夠保證每個牌的位置具有機會均等性呢?
首先一個常識是:n張牌,利用隨機數產生N種情況,則必須滿足N能夠整除n,這樣就能給予每個牌以N/n的機會(或者說權值),如果N不能整除n,必然機會不均等,即有些牌分配的機會多,有些少。
我們知道100的全排列有100的階乘種情況,而調用100次隨機函數,共可以產生100100種情況,而nn 必然不能整除n!,具體證明不在這里敘述。
那我們可以利用第二種方法改進,每次不是產生1100的隨機數,而是1i的數字,則共有n!中情況,即N=n!,顯然滿足條件,且時間為O(n),空間為O(1).這也就是Fisher-Yates_shuffle
算法,大多數庫都使用的這種方法。
我們看看java中Collections實現:
public static void shuffle(List<?> list, Random rnd) {
int size = list.size();
if (size < SHUFFLE_THRESHOLD || list instanceof RandomAccess) {
for (int i=size; i>1; i--)
swap(list, i-1, rnd.nextInt(i));
} else {
Object arr[] = list.toArray();
// Shuffle array
for (int i=size; i>1; i--)
swap(arr, i-1, rnd.nextInt(i));
// Dump array back into list
// instead of using a raw type here, it's possible to capture
// the wildcard but it will require a call to a supplementary
// private method
ListIterator it = list.listIterator();
for (int i=0; i<arr.length; i++) {
it.next();
it.set(arr[i]);
}
}
}
除了首先判斷能否隨機訪問,剩下的就是以上算法的實現了。
STL中實現:
// random_shuffle
template <class _RandomAccessIter>
inline void random_shuffle(_RandomAccessIter __first,
_RandomAccessIter __last) {
__STL_REQUIRES(_RandomAccessIter, _Mutable_RandomAccessIterator);
if (__first == __last) return;
for (_RandomAccessIter __i = __first + 1; __i != __last; ++__i)
iter_swap(__i, __first + __random_number((__i - __first) + 1));
}
template <class _RandomAccessIter, class _RandomNumberGenerator>
void random_shuffle(_RandomAccessIter __first, _RandomAccessIter __last,
_RandomNumberGenerator& __rand) {
__STL_REQUIRES(_RandomAccessIter, _Mutable_RandomAccessIterator);
if (__first == __last) return;
for (_RandomAccessIter __i = __first + 1; __i != __last; ++__i)
iter_swap(__i, __first + __rand((__i - __first) + 1));
}
如何測試洗牌算法具有隨機性呢?其實很簡單,調用洗牌算法N次,牌數為n,統計每個數字出現在某個位置的出現次數,構成一個矩陣n * n,如果這個矩陣的值都在N/n左右,則洗牌算法好。比如有100個數字,統計一萬次,則每個數字在某個位置的出現次數應該在100左右。
洗牌算法的應用也很廣,比如三國殺游戲、斗地主游戲等等。講一個最常見的場景,就是播放器的隨機播放。有些播放器的隨機播放,是每次產生一個隨機數來選擇播放的歌曲,這樣就有可能還沒有聽完所有的歌前,又聽到已經聽過的歌。另一種就是利用洗牌算法,把待播放的歌曲列表shuffle。如何判斷使用的是哪一種方案呢? 很簡單,如果點上一首還能回去,則利用的是洗牌算法,如果點上一首又是另外一首歌,則說明使用的是隨機產生方法。比如上一首是3,現在是18,點上一首,如果是3說明采用的洗牌算法,如果不是3,則說明不是洗牌算法(存在誤判,多試幾次就可以了)。
順便提一下網上的一些抽獎活動,尤其是轉盤,是不是真正的隨機?答案留給看客!