產生無重複的隨機數,注意,是
不重複的序列.
通常的產生隨機數的做法是不考慮重複的,因為即使重複也屬於機率意義上的正常情況.但某些情況下需要不重複的隨機資料,怎麼辦呢?
我想從大方向上來說,應該只有兩個方法.要麼犧牲時間要麼犧牲空間.講得不對或不完整,大家一定要指出來啊,謝謝.
注意,下面均以在101~200的範圍內(設為b[100],它實際上是附加空間),從中產生10個不重複的隨機數(設為a[10]).
一.犧牲時間為代價
這種方法不需要附加空間b數組.
要產生一定範圍內不可重複的隨機數,把曾經產生的隨機數儲存起來作為曆史資料。產生一個新的隨機數後在曆史資料搜尋,若找到就重新產生一個新的再重複資料搜尋;否則就認為已經找到了一個新的不同隨機數。
可以預見,每個新產生的隨機數都要與前面所有的數比較.若重複,捨棄,再產生;否則,產生下一個.平均耗時n的平方量級.
粗看起來,上面的程式似乎沒有什麼問題,在執行過程中程式也能夠通過。但,仔細分析我們就會發現問題出在一個新產生的隨機數是否已經存在的判定上。既然是隨機數,那麼從數學的角度來說在機率上,每次產生的隨機數 r就有可能相同,儘管這種可能性很小,但確是一個邏輯性與正確性的問題。因此,每次產生的新的隨機數r都有可能是數組random的前i-1個數中的某一個,也就是說程式在運行過程中由此可能會導致死迴圈!
有人可能會爭辯說,這種機率很小嘛,幾乎為零.的確,但我要問,演算法的五大特性是什麼,其中兩大特性就是:確定性和有窮性.
所以,怎麼解決?犧牲空間.(稍後介紹)
二.犧牲空間為代價
以下方法需要附加空間b數組.
(1)將範圍數組b[100](b[i]=100+i,不妨設數組下標從1開始)的每個元素設定一個標誌位flag.初始均為flag=0;若某元素被選入到a數組中,則flag=1;顯然,以後再選到重複元素可以立刻判定是否已選.這不正是以空間換時間嗎?
但是仍然有一個很嚴重的問題,在小規模輸入下,無疑它的表現是不錯的.但現在舉一個失敗的例子.
在1~65536之間,選擇65500個不重複的隨機數.看看後面的隨機數,比如第65500個數(最後一個),它要在剩下的36個數中選擇才會有flag=0(根本不知道這36個數是什麼);哼哼,機率36/65536.越到後面,隨機數越難產生,空間也換不了時間.
改進:先在1~65536之間隨機選取36個數,刪除.將剩下的65500個數依次賦值給a[65500],然後打亂順序即可,如下偽碼:
1 for i ← 1 to length[a]
2 do j ← random() // 隨機產生一個a數組的下標
3 exchange a[i]←→a[j] // 交換a[i]與a[j]
4
當範圍數組與目標數組的大小非常接近時,上述演算法非常有效,建議採用.
(2)問題的最終解決.
仍以最開始的那個例子來說,初始數組b[i]=100+i,a數組空.
每次隨機產生數組b的一個下標subscript,然後取出它所對應的資料a[subscript],記下來.然後將數組b的最後一個數b[length]放到下標subscript的位置,同時將數組a長度減1。儘管前若干次產生的下標subscript隨機數有可能相同,但,因為每一次都把最後一個數填到取出的位置,因此,相同下標subscript對應的數卻絕不會相同,每一次取出的數都不會一樣,這樣,就保證了演算法的確定性、有效性、有窮性.
偽碼演算法如下:
1 lower ← 101
2 upper ← 200
3 for i ← 1 to upper - lower + 1
4 do b[i] = lower + i - 1
5 for i← 1 to length[a]
6 do subscript = ( int )(length[b] * Rnd + lower) // 隨機產生b數組的一個下標,Rnd產生0~1隨機數
7 temp ← b[subscript]
8 b[subscript] ← b[length[b]]
9 length[b] -- ;
10 a[i] = temp;
11