開発中に、配列の順序を一様にランダムにしたいことがあります。
JavaScriptで配列をランダムにしようとした時に、↓のようなコードを見たことはありますか?
array.sort(() => Math.random() - 0.5);
Math.random() - 0.5 は -0.5 以上 0.5 未満の値を返します。
この値の正負を sort の比較結果として使うので、一見すると一様にランダムに並び替えられているように見えます。
でも実は、一様にランダムにはなりません。
結論:sortにrandomはNG
先ほどの書き方は
- 並びが偏る(バイアスがかかる)
- 実装によって結果が変わる
- 比較関数として期待される性質を満たさない
という点で問題があります。
要は、「なんとなく動く」けど「保証されない」コードです。
なぜ一様にランダムにならない?
ポイントは Array.prototype.sort の仕様にあります。
sortの比較関数の前提
array.sort((a, b) => {
return 負の値 or 0 or 正の値;
});
この比較関数には暗黙のルールがあります。
- 同じ入力なら同じ結果を返す(決定的であること)
- 順序関係が一貫していること(推移律など)
ここで、 Math.random() を使うとどうなるのか?
(a, b) => Math.random() - 0.5
Math.random() は呼び出すたびに異なる値を返しうるので、
-
a < bのときもあれば -
b < aのときもある
つまり、比較関数として成立していません。
ソートアルゴリズムとの相性が最悪
JavaScript のエンジン(V8 や SpiderMonkey など)は、効率のためにそれぞれ異なる sort の内部実装を持っています。
ただし、どの実装であっても、「比較関数が同じ 2 要素に対して一貫した結果を返すこと」は前提です。
なので、ランダム比較を渡すと
- 同じ要素同士の順序が矛盾する
- アルゴリズムが想定外の分岐をする
- 結果として偏った並びになる
といったことに陥ります。
実際どれくらい偏る?
簡単な例で考えてみます。
[1, 2, 3].sort(() => Math.random() - 0.5);
理論上は
[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]
の6通りの順列が均等に出てほしいですが、実際の結果は
- 特定の順番が出やすい
- ほとんど出ない順番もある
ランダムっぽいだけです。
正しいシャッフル方法:Fisher-Yates
一様にランダムにしたいなら、定番の方法があります。
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
いわゆる Fisher-Yates shuffleです。
- 各要素が均等な確率で並び替えられる
- アルゴリズム的に正しい
- 実装もシンプル
なのが特徴です。
本題:ECMAScriptの設計思想
この問題は、単なる「アルゴリズムの話」ではないと思います。
重要なのは仕様がどこまで責任を持つかという設計思想です。
ECMAScriptは「最低限の契約」しか定めない
ECMAScriptはsortの内部実装をエンジンに委ねている部分がありますが、現在は安定ソートであること自体は仕様で保証されています。
一方で、比較関数が一貫した順序を定義しているかどうかまでは実行時に厳密チェックしません。つまり、比較関数の契約を守るのは呼び出し側の責任です。
つまり、
「正しい比較関数を渡すことはユーザーの責任」
というスタンスです。
なぜそこまで割り切るのか?
こうした設計には、少なくとも次のような意図があると考えられます。
1. 実装の自由度を確保するため
エンジンごとに最適なアルゴリズムを使えるようにするためです。
2. パフォーマンス重視
仕様でガチガチに縛ると最適化できなくなる。
3. シンプルさの維持
言語仕様はできるだけミニマルに保つ。
正しくないコードも動く
JavaScriptは「柔軟でゆるい言語」と言われることがあります。
この件もまさにそれで、
array.sort(() => Math.random() - 0.5);
- エラーにはならない
- でも結果は保証されない
つまり、
契約違反のコードでもすぐにエラーにせず、結果の保証まではしない
これがECMAScriptの基本的な姿勢です。
まとめ
-
sort(() => Math.random() - 0.5)は一様にランダムにはならない -
理由は、比較関数として一貫した順序を定義できず、sortメソッドの前提に反するため
-
正しくシャッフルするなら Fisher-Yates を使う
-
ECMAScriptは、正しい使い方を前提にする設計
採用拡大中!
アシストエンジニアリングでは一緒に働くフロントエンド、バックエンドのエンジニアを募集しています!
少しでも興味ある方は、カジュアル面談からでもぜひお気軽にお話ししましょう!
お問い合わせはこちらから↓
https://official.assisteng.co.jp/contact/