何をしたいユーティリティか:「配列シャッフル」
ここでの「配列シャッフル」は、配列の要素の順番をランダムに並べ替える処理です。
例えばこうです。
[1, 2, 3, 4, 5]
// ↓ シャッフル
[3, 1, 5, 2, 4]
JavaScript業務だと、例えば次のような場面で使います。
- アンケートの選択肢を毎回ランダムな順番で表示したい
- A/B テスト用に、ユーザーをランダムにグループ分けしたい
- テストデータの順番をランダムにして、順序依存のバグをあぶり出したい
ここでは、「正しいシャッフルのやり方」と「やってはいけないシャッフル」を対比しながら、丁寧に説明していきます。
やってはいけないシャッフル:sort と Math.random
一見それっぽいけどダメな例
まず、よく見かけるけれどおすすめできない書き方から。
const arr = [1, 2, 3, 4, 5];
arr.sort(() => Math.random() - 0.5);
JavaScriptこれ、一見「ランダムに並び替えてくれそう」に見えますが、問題があります。
- 並び方の偏りが出る(すべての並び順が同じ確率にならない)
- sort の実装依存で挙動が変わる可能性がある
「なんとなくシャッフルされているように見える」だけで、
「ちゃんとしたランダムシャッフル」とは言えません。
業務で「ランダム」を扱うときは、「見た目それっぽい」ではなく「アルゴリズムとして正しい」ものを選ぶのが大事です。
正しいシャッフル:Fisher–Yates シャッフル
アルゴリズムのざっくりイメージ
Fisher–Yates シャッフル(フィッシャー–イェーツ)は、
「すべての並び順が同じ確率で選ばれる」ことが保証されたシャッフル方法です。
やっていることはシンプルで、
- 配列の末尾から順に見ていく
- 「今見ている位置」までの範囲からランダムに 1 つインデックスを選ぶ
- その 2 つの要素を入れ替える
これを先頭まで繰り返します。
実装例:インプレースでシャッフルする
function shuffleInPlace(array) {
if (!Array.isArray(array)) {
return [];
}
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
return array;
}
JavaScript重要なポイントを一つずつかみ砕く
末尾から 0 まで「逆向き」にループする理由
for (let i = array.length - 1; i > 0; i--) {
...
}
JavaScripti は「今から確定させる位置」です。
末尾(length - 1)から始めて、1 つずつ前に進んでいきます。
なぜ逆向きかというと、
- 末尾の要素は「どこから来てもいい」ので、最初にランダムに決めてしまう
- 一度決めた位置はもう触らない(確定)
- 残りの範囲だけをシャッフルしていく
という考え方だからです。
ランダムに選ぶインデックス j の範囲
const j = Math.floor(Math.random() * (i + 1));
JavaScriptここが一番大事なところです。
Math.random() は 0 以上 1 未満のランダムな数を返します。
それに (i + 1) を掛けて Math.floor することで、0 から i までの整数をランダムに 1 つ選んでいます。
つまり、「今見ている位置 i までのどこか」をランダムに選んでいるわけです。
要素の入れ替え(swap)
const tmp = array[i];
array[i] = array[j];
array[j] = tmp;
JavaScript選んだ 2 つの位置 i と j の要素を入れ替えています。
これを i を減らしながら繰り返すことで、
- 最初のループで「末尾の要素」がランダムに決まる
- 次のループで「末尾から 2 番目の要素」がランダムに決まる
- …
- 最後に「先頭の要素」が自動的に決まる
という流れになります。
実際の動き
const arr = [1, 2, 3, 4, 5];
shuffleInPlace(arr);
// 例: [3, 5, 1, 4, 2]
shuffleInPlace(arr);
// 例: [4, 1, 5, 2, 3]
JavaScript毎回違う順番になりますが、
どの並び順も「同じ確率」で出てくる、というのが Fisher–Yates の良さです。
元の配列を壊したくない場合:コピーしてシャッフル
イミュータブルなシャッフル
shuffleInPlace は「配列そのものを書き換える」関数です。
元の配列を残しておきたい場合は、「コピーを作ってからシャッフル」する関数を用意します。
function shuffled(array) {
if (!Array.isArray(array)) {
return [];
}
const copy = array.slice();
shuffleInPlace(copy);
return copy;
}
JavaScript実際の動き
const original = [1, 2, 3, 4, 5];
const s1 = shuffled(original);
const s2 = shuffled(original);
original; // [1, 2, 3, 4, 5] のまま
s1; // 例: [3, 1, 5, 2, 4]
s2; // 例: [2, 5, 1, 4, 3]
JavaScript業務コードでは、「元データはそのまま」「表示用だけシャッフルしたい」ことが多いので、shuffleInPlace と shuffled の両方を用意しておくと便利です。
実務で意識してほしい設計のポイント
「ランダム性の質」を軽くでも意識する
業務で「ランダム」を使うとき、
「なんとなくバラけていればいいや」で済む場面もあれば、
「偏りがあると困る」場面もあります。
- テストデータの順番 → 多少偏っても大きな問題にはなりにくい
- 抽選・割り当て・A/B テスト → 偏りがあると不公平・不正確になる
後者のような場面では、sort(() => Math.random() - 0.5) のような「なんちゃってシャッフル」は避けて、
Fisher–Yates のような「アルゴリズムとして正しいシャッフル」を使うべきです。
「どこまでランダムにするか」を決める
ときどき、「全部を完全にシャッフルする」のではなく、
「先頭から何件かだけランダムに選びたい」こともあります。
例えば、「ユーザー一覧からランダムに 10 人だけ選ぶ」ようなケースです。
その場合は、
const shuffledUsers = shuffled(users);
const picked = shuffledUsers.slice(0, 10);
JavaScriptのように、「シャッフル+先頭 N 件」という形にすると、
「偏りの少ないランダムサンプリング」に近づきます。
テストで「ランダム」をどう扱うか
ランダムシャッフルをテストするときは、
「結果の順番が毎回変わる」ことがテストの書きづらさにつながります。
その場合は、
- ランダムの種(seed)を固定できるライブラリを使う
- 「結果の順番」ではなく「結果が元の要素の並べ替えになっているか」を検証する
といった工夫をします。
例えば、「シャッフル後の配列をソートして、元の配列をソートしたものと一致するか」を見る、などです。
少し手を動かして感覚をつかむ
コンソールで、次のようなコードを実際に打ってみてください。
const arr = [1, 2, 3, 4, 5];
shuffleInPlace(arr);
arr;
const original = [1, 2, 3, 4, 5];
const s1 = shuffled(original);
const s2 = shuffled(original);
original;
s1;
s2;
JavaScript何度か実行してみて、
- 毎回違う順番になっていること
shuffledを使うと元の配列が変わらないこと
を自分の目で確認してみてください。
そのうえで、自分のプロジェクトに
export function shuffleInPlace(...) { ... }
export function shuffled(...) { ... }
JavaScriptを置き、
「配列をランダムにしたくなったら、必ずこの“配列シャッフルユーティリティ”を通す」
というルールを作ってみてください。
それだけで、あなたの「ランダム処理」は、なんとなくの書き方から、意図と根拠のある業務レベルの実装に変わっていきます。
