目次
概要
クイズアプリの出題順、ビンゴ大会、あるいはプレゼンの発表順など、データの並びをランダムにしたい場面は多々あります。
単純に 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)が向上します。
- 特定の要素を固定:「最初の挨拶(司会)」や「最後の締め」が決まっている場合は、その要素を除外してシャッフルし、最後に結合するロジックを追加します。
注意点
- 元の配列への影響上記のコードでは
[...array]でコピーを作成しているため安全ですが、コピーせずに直接引数の配列を操作すると、呼び出し元のデータも変わってしまいます(破壊的変更)。基本的にはコピーを作成して返す設計が推奨されます。 - 暗号学的なランダム性
Math.random()は疑似乱数です。パスワード生成やカジノゲームなど、極めて高いセキュリティレベルのランダム性が必要な場合は、crypto.getRandomValues()を使用する必要があります。
応用
偏りのある sort ランダムの検証
なぜ sort を使ってはいけないのかを確認するコードです。
// 注意: この方法は推奨されません
const badShuffle = (arr) => arr.sort(() => Math.random() - 0.5);
// 多くのブラウザで、特定の並びが出やすくなる傾向があります
まとめ
配列をシャッフルする場合は、自己流の実装や sort のハックを使わず、フィッシャー–イェーツのシャッフル を使用するのが鉄則です。
- アルゴリズム: 末尾からランダムに入れ替える。
- 実装:
forループとMath.random()、分割代入を使う。 - 安全性: 元の配列を壊さないよう、コピーに対して操作を行う。
この関数を一つ持っておけば、あらゆるランダム表示の要件に対応できます。
