そもそも「安定ソート」とは何か
まず言葉から整理します。
安定ソート(stable sort)とは、「同じキー(同じ値)同士の元々の順番を壊さないソート」のことです。
もう少し具体的に言うと、
- 並び替えの基準になる値(例:年齢、ステータス)が同じ要素が複数ある
- その要素たちの「元の順番」が、ソート後もそのまま保たれる
この性質を持つソートを「安定ソート」と呼びます。
逆に、同じキー同士の順番が入れ替わってしまうソートは「不安定ソート」です。
業務では、
「まずステータスでソートして、その中で登録日時の順番は保ちたい」
「優先度で並べるけど、同じ優先度なら元の順番のままがいい」
といった場面がよくあります。
こういうときに「安定ソート」が効いてきます。
JavaScript の sort と安定性
結論から
最近の主要な JavaScript 実装(モダンブラウザや Node.js)では、Array.prototype.sort は仕様として安定ソートであることが求められています。
ただし、「古い環境」や「実装依存だった時代」もありました。
業務ユーティリティとしては、
- 「環境に関わらず安定ソートである」と自分で保証したい
- ついでに「非破壊(元の配列を壊さない)」にしたい
という理由で、「安定ソートユーティリティ」を用意しておく価値があります。
基本形:インデックスを持たせて安定ソートを実現する
アイデアの核心
安定ソートを自前で保証する一番シンプルなテクニックは、
「元のインデックスを一緒に持たせてソートし、比較で同値だったらインデックスで決める」
というやり方です。
流れを言葉で書くと、
- 各要素に「元のインデックス」をくっつけた一時配列を作る
- ソートするとき、まず「ソートキー」で比較する
- ソートキーが同じなら、「元のインデックス」で比較する(小さいほうを先に)
- 最後に元の要素だけを取り出す
こうすると、「同じキー同士は元の順番のまま」になるので、安定ソートが保証されます。
安定ソートユーティリティ stableSort
function stableSort(array, compareFn) {
if (!Array.isArray(array)) {
return [];
}
const indexed = array.map((value, index) => ({ value, index }));
indexed.sort((a, b) => {
const order = compareFn(a.value, b.value);
if (order !== 0) {
return order;
}
return a.index - b.index;
});
return indexed.map((item) => item.value);
}
JavaScriptここでの重要ポイントを丁寧に分解します。
array.map((value, index) => ({ value, index }))
元の要素valueと、その位置indexをペアにしたオブジェクト配列を作っています。compareFn(a.value, b.value)
ユーザーが渡した比較関数で、通常のソート順を決めます。if (order !== 0) return order;
比較結果が 0 でない(=大小が決まった)なら、そのまま返します。return a.index - b.index;
比較結果が 0(=同じキー)だった場合は、「元のインデックスが小さいほうを先に」することで、元の順番を保ちます。- 最後に
indexed.map((item) => item.value)で、元の値だけを取り出して返します。
これで、「どんな環境でも安定ソートになる」ユーティリティが完成です。
例題1:年齢でソートしつつ、同じ年齢なら元の順番を保つ
元データ
const people = [
{ name: "A", age: 30 },
{ name: "B", age: 20 },
{ name: "C", age: 20 },
{ name: "D", age: 40 },
];
JavaScriptここで、「年齢の昇順でソートしたい。ただし、同じ年齢同士の順番は元のままがいい」とします。
安定ソートを使う
const sorted = stableSort(people, (a, b) => a.age - b.age);
JavaScript結果はこうなります。
[
{ name: "B", age: 20 },
{ name: "C", age: 20 },
{ name: "A", age: 30 },
{ name: "D", age: 40 },
]
JavaScriptここで注目してほしいのは、「B と C の順番」です。
- 元の配列では
B → Cの順 - ソート後も
B → Cの順のまま
これが「安定ソート」の性質です。
もし不安定なソートだと、C → B の順に入れ替わってしまう可能性があります。
例題2:ステータス → 作成日時の順で並べたい
業務でよくある「複合条件ソート」を、安定ソートで分かりやすく書いてみます。
元データ
const items = [
{ id: 1, status: "done", createdAt: "2024-01-01" },
{ id: 2, status: "todo", createdAt: "2024-03-01" },
{ id: 3, status: "doing", createdAt: "2024-02-01" },
{ id: 4, status: "todo", createdAt: "2024-01-15" },
{ id: 5, status: "done", createdAt: "2024-02-10" },
];
JavaScript要件:
- ステータスの優先度は
todo→doing→doneの順 - 同じステータスの中では、作成日時の昇順(古い順)
比較関数を定義する
const statusOrder = {
todo: 1,
doing: 2,
done: 3,
};
function compareByStatusThenDate(a, b) {
const sa = statusOrder[a.status];
const sb = statusOrder[b.status];
if (sa !== sb) {
return sa - sb;
}
const da = new Date(a.createdAt);
const db = new Date(b.createdAt);
return da - db;
}
JavaScriptここでは、「ステータスの優先度 → 日付」の順で比較しています。stableSort と組み合わせるとこうなります。
const sorted = stableSort(items, compareByStatusThenDate);
JavaScriptこのとき、「ステータスも日付も同じ」要素があったとしても、
元の順番が保たれることが保証されます。
非破壊であることの大事さ
stableSort は、元の配列を一切書き換えません。
内部で map して別配列を作り、その配列をソートしてから値を取り出しています。
業務コードでは、
- API から来た生データはそのまま保持しておきたい
- 画面表示用や集計用に「ソート済みの別配列」を使いたい
という場面がとても多いです。
array.sort(...) をそのまま使うと、元の配列が書き換わってしまうので、
- 「どこで順番が変わったのか分からない」
- 「別の処理が想定外の順番で動き始める」
といったバグにつながりやすくなります。
stableSort のように「必ず新しい配列を返す」ユーティリティを使うことで、
「元データは不変」「加工結果は別」という設計を徹底できます。
初心者向けにもう一度:安定ソートのイメージを固める
イメージしやすいように、もっとシンプルな例で確認してみましょう。
const data = [
{ label: "A", group: 1 },
{ label: "B", group: 2 },
{ label: "C", group: 1 },
{ label: "D", group: 2 },
];
JavaScript「group の昇順でソートしたい」とします。
const sorted = stableSort(data, (a, b) => a.group - b.group);
JavaScript結果はこうなります。
[
{ label: "A", group: 1 },
{ label: "C", group: 1 },
{ label: "B", group: 2 },
{ label: "D", group: 2 },
]
JavaScriptここでのポイントは、
- group=1 の中で、元の順番は
A → C→ ソート後もA → C - group=2 の中で、元の順番は
B → D→ ソート後もB → D
「グループ内の順番はそのまま」というのが、安定ソートの感覚です。
手を動かして試してみる
コンソールで、次のコードをそのまま貼って動かしてみてください。
function stableSort(array, compareFn) {
if (!Array.isArray(array)) {
return [];
}
const indexed = array.map((value, index) => ({ value, index }));
indexed.sort((a, b) => {
const order = compareFn(a.value, b.value);
if (order !== 0) {
return order;
}
return a.index - b.index;
});
return indexed.map((item) => item.value);
}
const data = [
{ label: "A", group: 1 },
{ label: "B", group: 2 },
{ label: "C", group: 1 },
{ label: "D", group: 2 },
];
const sorted = stableSort(data, (a, b) => a.group - b.group);
console.log(sorted);
console.log(data); // 元の配列が変わっていないことも確認
JavaScript「同じ group の中で順番が保たれていること」「元の配列が変わっていないこと」を、自分の目で確認してみてください。
まとめ:安定ソートユーティリティをプロジェクトに置く意味
安定ソートは、単に「きれいに並べる」だけではなく、
- 同じキー同士の順番を壊さない(仕様として重要なことが多い)
- 元の配列を壊さない(非破壊)
- 比較関数を渡すだけで、どんな業務ロジックにも対応できる
という性質を持った、かなり実務寄りの武器です。
プロジェクトに
export function stableSort(...) { ... }
JavaScriptを一つ置いておき、「配列を業務ルールで並べ替えたいときは、まずこれを使う」と決めてしまうと、
ソート周りのバグや「順番がいつの間にか変わっている問題」がかなり減ります。
「ただ sort する」のではなく、「安定に、かつ非破壊で sort する」という感覚を、ここで一度しっかり掴んでおくと、この先の配列処理が一段レベルアップします。
