JavaScript Tips | 配列ユーティリティ:配列シャッフル

JavaScript JavaScript
スポンサーリンク

何をしたいユーティリティか:「配列シャッフル」

ここでの「配列シャッフル」は、配列の要素の順番をランダムに並べ替える処理です。

例えばこうです。

[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. 「今見ている位置」までの範囲からランダムに 1 つインデックスを選ぶ
  3. その 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--) {
  ...
}
JavaScript

i は「今から確定させる位置」です。
末尾(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 つの位置 ij の要素を入れ替えています。

これを 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

業務コードでは、「元データはそのまま」「表示用だけシャッフルしたい」ことが多いので、
shuffleInPlaceshuffled の両方を用意しておくと便利です。


実務で意識してほしい設計のポイント

「ランダム性の質」を軽くでも意識する

業務で「ランダム」を使うとき、
「なんとなくバラけていればいいや」で済む場面もあれば、
「偏りがあると困る」場面もあります。

  • テストデータの順番 → 多少偏っても大きな問題にはなりにくい
  • 抽選・割り当て・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

を置き、

「配列をランダムにしたくなったら、必ずこの“配列シャッフルユーティリティ”を通す」

というルールを作ってみてください。
それだけで、あなたの「ランダム処理」は、なんとなくの書き方から、意図と根拠のある業務レベルの実装に変わっていきます。

タイトルとURLをコピーしました