1.Fisher–Yates Shuffle(費雪耶茲 隨機置亂演算法)
演算法思想就是從原始數組中隨機抽取一個新的數字到新數組中。演算法英文描述如下:
- Write down the numbers from 1 through N.
- Pick a random number k between one and the number of unstruck numbers remaining (inclusive).
- Counting from the low end, strike out the kth number not yet struck out, and write it down elsewhere.
- Repeat from step 2 until all the numbers have been struck out.
- The sequence of numbers written down in step 3 is now a random permutation of the original numbers.
演算法描述:
Let X1, X2…. XN be the set of N numbers to be shuffled.
- Set j to N
- Generate a random number R. (uniformly distributed between 0 and 1)
- Set k to (jR+1). k is now a random integer, between 1 and j.
- Exchange Xk and Xj
- Decrease j by 1.
- If j > 1, return to step 2.
golang代碼實現如下:
package shuffleimport ( "fmt")func FisherYatesShuffle(array []int) []int { newArray := make([]int, 0) for { Loop: length := len(array) if length == 0 { break } for i := 0; i <= length; i++ { p := RandInt64(0, int64(length)) fmt.Printf("每次產生的隨機數:%d\n", p) newArray = append(newArray, array[p]) array = append(array[0:p], array[p+1:]...) goto Loop } } return newArray}package shuffleimport ( "fmt" "testing")func TestFisherYatesShuffle(t *testing.T) { array := []int{1, 2, 3, 4, 5} shuffle := FisherYatesShuffle(array) fmt.Println(shuffle)}
運行結果:
每次產生的隨機數:0產生的新數組:[1]每次產生的隨機數:3產生的新數組:[1 5]每次產生的隨機數:0產生的新數組:[1 5 2]每次產生的隨機數:1產生的新數組:[1 5 2 4]每次產生的隨機數:0產生的新數組:[1 5 2 4 3]
2. Knuth-Durstenfeld Shuffle
Knuth 和Durstenfeld 在Fisher 等人的基礎上對演算法進行了改進。每次從未處理的資料中隨機取出一個數字,然後把該數字放在數組的尾部,即數組尾部存放的是已經處理過的數字。這是一個原地打亂順序的演算法,演算法時間複雜度也從Fisher演算法的O(n2)提升到了O(n)。
golang代碼實現如下:
package shuffleimport ( "fmt" "math/rand")func KnuthDurstenfeldShuffle(array []int) []int { for i := len(array) - 1; i >= 0; i-- { p := RandInt64(0, int64(i)) fmt.Printf("每次產生的隨機數:%d\n", p) a := array[i] array[i] = array[p] array[p] = a fmt.Println("置換後的數組為:", array) } return array}// RandInt64 區間隨機數func RandInt64(min, max int64) int64 { if min >= max || max == 0 { return max } return rand.Int63n(max-min) + min}package shuffleimport ( "fmt" "testing")func TestKnuthDurstenfeldShuffle(t *testing.T) { array := []int{1, 2, 3, 4, 5} shuffle := KnuthDurstenfeldShuffle(array) fmt.Println(shuffle)}
運行結果:
每次產生的隨機數:2置換後的數組為: [1 2 5 4 3]每次產生的隨機數:1置換後的數組為: [1 4 5 2 3]每次產生的隨機數:1置換後的數組為: [1 5 4 2 3]每次產生的隨機數:0置換後的數組為: [5 1 4 2 3]每次產生的隨機數:0置換後的數組為: [5 1 4 2 3][5 1 4 2 3]
從運行結果可以看到隨著演算法的運行,可供選擇的隨機數範圍在減小,與此同時此時數組裡的元素更趨向於無序。
潛在的偏差:
在實現Fisher-Yates費雪耶茲隨機置亂演算法時,可能會出現偏差,儘管這種偏差是非常不明顯的。原因:一是實現演算法本身出現問題;二是演算法基於的隨機數產生器。
- 1 . 實現上每一種排列非等機率出現
在演算法流程裡 j 的選擇範圍是從 0...i-1 ;這樣Fisher-Yates演算法就變成了Sattolo演算法,共有(n-1)!種不同的排列,而非n!種排列。
j在所有0...n的範圍內選擇,則一些序列必須通過n^n種排列才可能產生。
- 2 . Fisher-Yates費雪耶茲演算法使用的隨機數產生器是PRNG偽隨機數產生器
這樣的一個偽隨機數產生器產生的序列,完全由序列開始的內部狀態所確定,由這樣的一個偽隨機產生器驅動的演算法產生的不同置亂不可能多於產生器的不同狀態數,甚至當可能的狀態數超過了排列,不正常的從狀態數到排列的映射會使一些排列出現的頻率超過其他的。所以狀態數需要比排列數高几個量級。
很多語言或者庫函數內建的偽隨機數產生器只有32位的內部狀態,意味著可以產生2^32種不同的序列數。如果這樣一個隨機器用於置亂一副52張的撲克牌,只能產生52! = 2^225.6種可能的排列中的一小部分。對於少於226位的內部狀態的隨機數產生器不可能產生52張卡片的所有的排列。
偽隨機數產生器的內部狀態數和基於此產生器的每種排列都可以產生的最大線性表長度之間的關係:
shuffle.jpg
3.Inside-Out Algorithm
Knuth-Durstenfeld Shuffle 是一個in-place演算法,未經處理資料被直接打亂,有些應用中可能需要保留未經處理資料,因此需要開闢一個新數組來儲存打亂後的序列。Inside-Out Algorithm 演算法的基本思想是設一遊標i從前向後掃描未經處理資料的拷貝,在[0, i]之間隨機一個下標j,然後用位置j的元素替換掉位置i的數字,再用未經處理資料位置i的元素替換掉拷貝資料位元置j的元素。其作用相當於在拷貝資料中交換i與j位置處的值。