【JavaScript】配列を完全にランダムに並べ替える!フィッシャー–イェーツのシャッフル実装

目次

概要

クイズアプリの出題順、ビンゴ大会、あるいはプレゼンの発表順など、データの並びをランダムにしたい場面は多々あります。

単純に Math.random() を使うだけでは「偏り」が生じやすく、公平なシャッフルにはなりません。本記事では、数学的に公平性が証明されている標準的なアルゴリズム「フィッシャー–イェーツのシャッフル(Fisher-Yates Shuffle)」を用いた実装方法を解説します。

仕様(入出力)

フィッシャー–イェーツのシャッフル

配列の末尾から先頭に向かって順番に、「その位置」と「それより前のランダムな位置」の要素を入れ替えていくアルゴリズムです。

  • 入力: 配列(任意のデータ型)
  • 出力: ランダムに並べ替えられた配列
  • 計算量: $O(n)$ (非常に高速)

注意すべき「やってはいけない」方法

よく見かける array.sort(() => Math.random() - 0.5) という方法は、厳密にはランダムにならず、並び順に偏りが出るため、実務では使用を避けてください。

基本の使い方

汎用的なシャッフル関数を定義して使用します。

/**
 * 配列をシャッフルする関数 (Fisher-Yates Shuffle)
 * @param {Array} sourceArr - 元の配列
 * @returns {Array} シャッフルされた新しい配列(元の配列は変更しないようにコピーを作成)
 */
const shuffleArray = (sourceArr) => {
    // 元の配列を破壊しないよう、スプレッド構文でコピーを作成
    const array = [...sourceArr];
    
    // 配列の末尾から先頭に向かってループ
    for (let i = array.length - 1; i >= 0; i--) {
        // 0 〜 i の範囲でランダムなインデックスを生成
        const randomIndex = Math.floor(Math.random() * (i + 1));
        
        // 分割代入を使って要素を入れ替え (Swap)
        [array[i], array[randomIndex]] = [array[randomIndex], array[i]];
    }
    
    return array;
};

// 使用例
const numbers = [1, 2, 3, 4, 5];
const shuffled = shuffleArray(numbers);
console.log(shuffled); // 例: [3, 1, 5, 2, 4]

コード全文(HTML / JavaScript)

社内のライトニングトーク(LT)大会における「発表者の登壇順」をランダムに決定するツールです。

ボタンを押すたびに、公平に並べ替えられたリストが生成されます。

HTML

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>LT Order Shuffle</title>
    <style>
        .shuffle-container {
            font-family: 'Hiragino Kaku Gothic ProN', sans-serif;
            max-width: 400px;
            padding: 25px;
            border: 1px solid #ddd;
            border-radius: 8px;
            background-color: #fafafa;
            text-align: center;
        }
        h3 { margin-top: 0; color: #444; }
        
        .speaker-list {
            list-style: decimal; /* 番号付きリスト */
            padding-left: 1.5em;
            text-align: left;
            margin: 20px auto;
            width: fit-content;
            font-size: 1.1rem;
        }
        .speaker-item {
            padding: 5px 0;
            border-bottom: 1px dashed #ccc;
        }
        
        .btn-shuffle {
            background-color: #e91e63;
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 30px;
            font-size: 1rem;
            font-weight: bold;
            cursor: pointer;
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
            transition: transform 0.1s, box-shadow 0.1s;
        }
        .btn-shuffle:active {
            transform: translateY(2px);
            box-shadow: none;
        }
    </style>
</head>
<body>

<div class="shuffle-container">
    <h3>LT大会 発表順決め</h3>
    
    <ol id="order-list" class="speaker-list">
        </ol>
    
    <button id="btn-shuffle" class="btn-shuffle">シャッフル実行 🎲</button>
</div>

<script src="lt_shuffle.js"></script>
</body>
</html>

JavaScript

/**
 * 発表順シャッフルスクリプト
 * Fisher-Yatesアルゴリズムの実装
 */

// 1. 発表者リスト(初期データ)
const speakers = [
    '森',
    '小森',
    '中森',
    '大森',
    '林',
    '小林',
    '中林',
    '大林'
];

// DOM要素
const listElement = document.getElementById('order-list');
const shuffleButton = document.getElementById('btn-shuffle');

/**
 * 配列をランダムに並べ替える関数
 * @param {Array} array 対象の配列
 * @returns {Array} シャッフルされた新しい配列
 */
const shuffleArray = (array) => {
    // 破壊的な操作を避けるためコピーを作成
    const clone = [...array];

    for (let i = clone.length - 1; i >= 0; i--) {
        // ランダムなインデックスを決定
        const rand = Math.floor(Math.random() * (i + 1));
        
        // 分割代入で要素を入れ替え
        [clone[i], clone[rand]] = [clone[rand], clone[i]];
    }
    
    return clone;
};

/**
 * リストを描画する関数
 * @param {Array} items 表示する配列
 */
const renderList = (items) => {
    listElement.innerHTML = '';
    
    items.forEach((item) => {
        const li = document.createElement('li');
        li.className = 'speaker-item';
        li.textContent = `${item} さん`;
        listElement.appendChild(li);
    });
};

/**
 * ボタンクリック時の処理
 */
const onShuffleClick = () => {
    // シャッフルを実行
    const newOrder = shuffleArray(speakers);
    
    // 画面を更新
    renderList(newOrder);
};

// 初期表示
renderList(speakers);

// イベントリスナー
shuffleButton.addEventListener('click', onShuffleClick);

カスタムポイント

  • アニメーション追加:シャッフル時に要素がパラパラと動くアニメーションを入れると、ユーザー体験(UX)が向上します。
  • 特定の要素を固定:「最初の挨拶(司会)」や「最後の締め」が決まっている場合は、その要素を除外してシャッフルし、最後に結合するロジックを追加します。

注意点

  1. 元の配列への影響上記のコードでは [...array] でコピーを作成しているため安全ですが、コピーせずに直接引数の配列を操作すると、呼び出し元のデータも変わってしまいます(破壊的変更)。基本的にはコピーを作成して返す設計が推奨されます。
  2. 暗号学的なランダム性Math.random() は疑似乱数です。パスワード生成やカジノゲームなど、極めて高いセキュリティレベルのランダム性が必要な場合は、crypto.getRandomValues() を使用する必要があります。

応用

偏りのある sort ランダムの検証

なぜ sort を使ってはいけないのかを確認するコードです。

// 注意: この方法は推奨されません
const badShuffle = (arr) => arr.sort(() => Math.random() - 0.5);

// 多くのブラウザで、特定の並びが出やすくなる傾向があります

まとめ

配列をシャッフルする場合は、自己流の実装や sort のハックを使わず、フィッシャー–イェーツのシャッフル を使用するのが鉄則です。

  • アルゴリズム: 末尾からランダムに入れ替える。
  • 実装: for ループと Math.random()、分割代入を使う。
  • 安全性: 元の配列を壊さないよう、コピーに対して操作を行う。

この関数を一つ持っておけば、あらゆるランダム表示の要件に対応できます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次