撲克牌 54 張,如何用 rand() 產生均勻隨機排列?介紹兩種演算法演進。
邏輯:對每個位置 i,從 全範圍 [0, 53] 隨機選一張交換。
void shuffle_naive(int *deck, int n) {
for (int i = 0; i < n; i++) {
int r = rand() % n; // 0 ~ n-1
int tmp = deck[i];
deck[i] = deck[r];
deck[r] = tmp;
}
}
缺陷:機率分佈不均勻。總排列數 n!,但此演算法產生 n^n 種等概率路徑,n^n 無法整除 n! (n>2 時),導致某些排列機率較高。且已洗好的牌會被再次交換,邏輯不直觀。
核心洞見:第 i 張牌只需從 剩餘未洗牌 [i, n-1] 中隨機選一張交換。已洗區段 [0, i-1] 確定不再動。
void shuffle_fisher_yates(int *deck, int n) {
for (int i = 0; i < n; i++) {
int r = i + rand() % (n - i); // i ~ n-1
int tmp = deck[i];
deck[i] = deck[r];
deck[r] = tmp;
}
}
1/n × 1/(n-1) × ... × 1/1 = 1/n! ✓| 陷阱 | 後果 | 修正 |
|---|---|---|
rand() % n 模數偏差 | RAND_MAX 不被 n 整除時,低數值機率略高 | 用拒絕採樣或 arc4random_uniform / C++11 uniform_int_distribution |
| 未設定種子 | 每次執行序列相同 | srand(time(NULL)) 或更強熵源 |
迴圈寫成 i < n-1 | 最後一張牌永不動 | i < n (最後一次 r=n-1 自我交換無害) |
#include
#include
std::random_device rd;
std::mt19937 g(rd());
std::shuffle(deck, deck + n, g); // 內部即 Fisher-Yates
教訓:直觀的「全域隨機交換」是錯的。演算法設計必須嚴謹證明機率分佈。Fisher-Yates 以 O(n) 時間、O(1) 空間、嚴格均勻,成為洗牌演算法的黃金標準。