公平洗牌的標準不是「牌序看起來很亂」,而是原本 n 個項目的每一種排列都具有相同機率,也就是各為 1 除以 n 階乘。Fisher–Yates 洗牌能用恰好 n 減 1 次交換達成這個目標:從尚未固定的位置中均勻選一個項目,放到目前最後一格,再逐步縮小範圍。它的原理簡單,但只要隨機索引範圍寫錯,或底層亂數不均勻,結果就會產生偏差。

看起來雜亂不等於機率公平

假設只有三張牌 A、B、C,一共有六種可能排列。公平演算法應讓 ABC、ACB、BAC、BCA、CAB、CBA 各自以六分之一機率出現。若某方法經常保留 A 在前面,縱使每次結果都不同,仍然不公平。

人眼不擅長判斷隨機性。我們容易把連續、對稱或重複視為可疑,卻把刻意避免這些形狀的序列當成「更亂」。真正均勻的洗牌本來就可能讓兩張原本相鄰的牌繼續相鄰,也可能偶爾回到原順序。公平描述的是長期分布,而不是單次輸出的外觀。

對抽獎、題目排序、配對與遊戲發牌而言,這項差異很實際。某些排列如果更常出現,玩家位置、題目順序或候選人就可能獲得系統性優勢。測試幾次「看起來沒問題」無法排除這種偏差。

Fisher–Yates 的操作方式

從尾端逐格固定

以五個項目為例,先看索引 0 至 4 的整個陣列。從這五個位置均勻抽出一個索引,將該項目與索引 4 的項目交換。此後最後一格確定,不再參與洗牌。

接著只看索引 0 至 3,均勻抽一個位置與索引 3 交換。然後範圍縮成 0 至 2,再縮成 0 至 1。剩下最後一格時,不必再做任何動作。這就是常見的原地版本,不需要建立第二份完整陣列,時間複雜度為線性,額外空間需求也很低。

關鍵在於每一輪的候選範圍都必須包含目前位置本身。例如處理索引 i 時,隨機索引 j 應從 0 至 i 中選取。選到 i 代表這一輪與自己交換,項目保持原位。若刻意排除自己,看似增加變動,反而破壞均勻性。

為什麼每個排列等機率

第一輪中,任何一個項目被放到最後一格的機率都是 1 除以 n。在已決定最後一格的條件下,第二輪讓其餘每個項目進入倒數第二格的機率是 1 除以 n 減 1。一路進行後,某個指定排列被建立的機率,是這些選擇機率相乘。

分母依序為 nn 減 1,一直到 2,乘積就是 n 階乘。每個指定排列都對應唯一一串選擇,因此機率同為 1 除以 n 階乘。這個證明同時說明為何範圍不能任意更改:每輪候選數一變,選擇路徑就不再與排列一一均勻對應。

常見但有偏差的寫法

每個位置都與全陣列隨機交換

一種常見做法,是依序走訪所有位置,每次都從整個陣列抽一個位置交換。它做了 n 次選擇,每次有 n 種可能,所以共有 nn 次方條選擇路徑。但排列總數是 n 階乘;在多數 n 下,前者不能平均分配給後者,因此某些排列必然對應較多路徑。

以三個項目來說,這種方法有 27 條等機率操作路徑,卻要分給六種排列。27 無法被 6 整除,所以不可能讓每個排列機率完全相同。增加交換次數或覺得「應該夠亂」都不能修正這個結構問題。

使用隨機排序比較器

有些程式把陣列交給排序函式,並讓比較器隨機回傳正負值。排序演算法預期比較關係具有一致性,例如 A 小於 B、B 小於 C 時,關係不能下一刻任意翻轉。隨機比較破壞這個前提,結果會依排序實作、呼叫順序和引擎版本而異,也通常不是均勻分布。

這種寫法雖短,卻難以推理和測試。正確的 Fisher–Yates 同樣不複雜,執行時間還可明確維持線性,沒有理由用不符合排序契約的方法代替。

忘記處理亂數上限

程式語言的亂數函式可能回傳半開區間,也可能包含兩端;整數轉換時若四捨五入而非向下取整,區間兩端的機率可能只有中間值的一半。實作前要確認 API 定義,確保 0 至 i 每個整數被選中的機率一致,且不會偶爾超出陣列。

另一個問題是用取餘數把大範圍整數壓成小範圍。如果亂數來源的可能值數量不是候選數的整數倍,部分餘數會多對應一個來源值,形成模數偏差。一般遊戲中影響可能很小,安全或高公平需求則應採用拒絕取樣等無偏方法。

亂數來源決定公平的上限

Fisher–Yates 證明建立在每輪都能均勻抽取整數之上。若偽隨機數產生器品質差、週期太短或種子可預測,演算法本身正確也無法補救。一般介面動畫與單機遊戲可使用平台提供的常規亂數;涉及獎品、金錢、密碼或不可預測性的場景,應使用作業系統提供的密碼學安全亂數。

「可重現」與「不可預測」是不同需求。測試與模擬常故意固定種子,方便重現同一牌序並追查錯誤;正式抽獎若公開或重複使用種子,第三方可能預測結果。設計系統時應先界定目標,而不是只在函式名稱裡看到 random 就假定足夠。

如何測試洗牌實作

最基本的正確性檢查,是確認輸出長度不變、每個項目恰好出現一次,沒有遺失或重複。接著可用小陣列重複執行很多次,統計所有排列的次數。三至五個項目時,排列總數仍容易枚舉,可用卡方等方法檢查分布是否出現明顯偏差。

對大型陣列,無法直接統計所有排列,因為排列數成長極快。可以改查每個項目落在各位置的頻率、成對項目的前後順序,以及某些結構事件的比例。不過,通過有限統計測試不等於數學上保證均勻;最可靠的方法仍是採用已證明正確的演算法,再以測試防止索引與邊界實作錯誤。

測試也要避免單次結果斷言。公平洗牌可能輸出原順序,因此「結果不得等於輸入」是錯誤測試。較好的單元測試可注入可控制的亂數序列,驗證每一步交換是否符合預期;分布測試則作為額外檢查,而非要求每批結果完美平均。

抽籤與輪盤的實務取捨

若每個參與者只能中選一次,可先用 Fisher–Yates 洗牌,再依序取出項目。這比每次反覆隨機抽取並檢查重複更直接,也容易證明所有順序等機率。若只是抽出少數名額,也可只執行前幾輪的部分洗牌,避免處理後面不需要的位置。

有權重的抽獎則不是均勻排列問題。當每個項目的中選機率不同,必須使用加權抽樣演算法,並明確定義中選後是否移除。不能先複製多份名稱再任意洗牌而忽略記憶體、精度與權重比例問題。公平首先來自規則清楚,其次才是正確實作。

結語

Fisher–Yates 的核心是「從尚未固定的項目中均勻選一個,放到目前位置」。候選範圍逐輪縮小,讓每種排列恰好對應一條等機率選擇路徑。隨機全域交換、隨機排序比較器與錯誤的整數映射,都可能讓某些排列更常出現。當底層亂數符合需求、索引邊界正確,這套線性演算法便能為洗牌、抽籤與遊戲排序提供清楚且可驗證的公平基礎。