Fisher-Yatesシャッフルアルゴリズム:ランダム並び替えに乱数だけでは不十分な理由

「シャッフル」ボタンを押したとき、結果は本当にランダムですか?多くの人は「乱数関数を呼ぶだけで公平なランダム順列が得られる」と思っていますが、実際には間違った実装によって特定の順列が他より多く出現する「隠れた偏り」が生じます。本記事では、正しいシャッフルアルゴリズムであるFisher-Yates Shuffleの仕組みを解説します。

1. 「公平なランダム順列」とは何か

3つの要素 [A, B, C] があるとすると、可能な順列は 3! = 6 通りです:

ABC  ACB  BAC  BCA  CAB  CBA

「公平なシャッフル」とは、これら6通りの順列がそれぞれ 1/6 ≈ 16.67% の確率で出現することを意味します。ある実装で ABC が20%の確率で出現し他が16%しか出ない場合、それは「ランダムに見える」だけで統計的には偏ったシャッフルです。

2. よくある落とし穴:sort() + Math.random()

多くの開発者が書いたことのあるコード(JavaScript):

// よく見るが偏りのある実装
array.sort(() => Math.random() - 0.5);

一見合理的に見えますが、実際には深刻な偏りがあります:

  • ソートアルゴリズムは一貫した比較を前提とする:クイックソートやマージソートは同じ2要素を複数回比較することがあります。ランダムな比較関数は毎回結果が変わるため、アルゴリズムが予期しないパスをたどります。
  • 数学的に均一分布を保証できない:比較ソートの計算量はO(n log n)で、n log n回の比較しか行いません。n個の要素にはn!通りの順列があり、n!がO(n log n)で区別できる状態数をはるかに超えると、一部の順列が「まとめられて」高い確率で出現します。
統計的検証
[1,2,3] に対してsort+randomを100万回実行すると、一部の順列が16.67%ではなく約20%出現することがわかります。偏りは小さいですが、公平性が求められる抽選では許容できません。

3. Fisher-Yatesアルゴリズムの仕組み

Fisher-Yates Shuffle(現代版はKnuth Shuffleとも呼ばれる)は1938年にRonald FisherとFrank Yatesが考案し、Donald Knuthが『The Art of Computer Programming』で現代的な形に発展させました。

アルゴリズムの手順(n要素の配列):

  1. 最後の要素(インデックス n-1)から始め、インデックス1まで逆方向に反復します。
  2. 現在のインデックス i に対して、[0, i](両端含む)から乱数でインデックス j を選びます。
  3. array[i]array[j] を交換します。
  4. i-1 で同じ処理を繰り返し、i = 1 まで続けます。
// Fisher-Yates Shuffle(JavaScript 正しい実装)
function shuffle(array) {
    const a = [...array]; // 元の配列を変更しない
    for (let i = a.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [a[i], a[j]] = [a[j], a[i]]; // 交換
    }
    return a;
}

4. なぜFisher-Yatesは均一分布を保証するのか

[A, B, C] を例に確率を分析します:

ステップ操作確率
i=2[0,1,2]からjを選ぶ(3番目の位置の要素を決定)各1/3
i=1[0,1]からjを選ぶ(2番目の位置の要素を決定)各1/2
i=01番目の位置には残りの要素が固定される1

任意の順列が出現する確率:

P(特定の順列) = 1/3 × 1/2 × 1 = 1/6

数学的に完全に均一です。n個の要素に対して、各順列の出現確率は正確に 1/n! となります(乱数関数自体が均一分布であることが前提)。

5. グループ分けへの応用

「ランダムグループ分け」は「シャッフル + 均等配分」の2ステップです:

function randomGroup(names, groupCount) {
    const shuffled = shuffle(names);               // ステップ1:Fisher-Yatesシャッフル
    const groups = Array.from({ length: groupCount }, () => []);
    shuffled.forEach((name, i) => {
        groups[i % groupCount].push(name);          // ステップ2:順番に各グループへ
    });
    return groups;
}

ラウンドロビン配分(i % groupCount)により、各グループの人数差は最大1人です。10人を3グループに分けると4・3・3人になり、4・4・2のような不均等な分配は起きません。

6. よくある誤りのまとめ

誤り問題正しい方法
array.sort(() => Math.random() - 0.5)統計的偏りFisher-Yatesを使う
各要素に乱数キーを割り当てて並べ替え理論上正しいがO(n log n)Fisher-YatesはO(n)
先頭k個だけをランダムに選ぶ完全なシャッフルではないFisher-Yatesの最初のk回=非復元抽出
同じ位置を複数回ランダムに交換交換確率が不均一各位置は1回だけ交換する
応用:部分シャッフル=非復元抽出
Fisher-Yatesの最初のk回だけ実行すると、n個の要素からk個を非復元でランダム抽出できます。計算量はO(k)で、配列全体をシャッフルする必要がありません。抽選や問題のランダム出題に最適です。

7. まとめ

シャッフルは単純に見えますが、「見た目がランダム」と「統計的に均一」は別物です。Fisher-YatesアルゴリズムはO(n)の計算量と数学的な均一性の保証を兼ね備えた、公平性が求められるあらゆる場面での標準的な選択肢です。

実際のランダムグループ分けを試したい方は、本サイトのリストグループ化ツールをご利用ください。名前を入力するだけで、公平なシャッフルやグループ分けが即座に行えます。