JavaScript Tips | 配列ユーティリティ:安定ソート

JavaScript JavaScript
スポンサーリンク

そもそも「安定ソート」とは何か

まず言葉から整理します。
安定ソート(stable sort)とは、「同じキー(同じ値)同士の元々の順番を壊さないソート」のことです。

もう少し具体的に言うと、

  • 並び替えの基準になる値(例:年齢、ステータス)が同じ要素が複数ある
  • その要素たちの「元の順番」が、ソート後もそのまま保たれる

この性質を持つソートを「安定ソート」と呼びます。

逆に、同じキー同士の順番が入れ替わってしまうソートは「不安定ソート」です。

業務では、
「まずステータスでソートして、その中で登録日時の順番は保ちたい」
「優先度で並べるけど、同じ優先度なら元の順番のままがいい」
といった場面がよくあります。
こういうときに「安定ソート」が効いてきます。


JavaScript の sort と安定性

結論から

最近の主要な JavaScript 実装(モダンブラウザや Node.js)では、Array.prototype.sort仕様として安定ソートであることが求められています
ただし、「古い環境」や「実装依存だった時代」もありました。

業務ユーティリティとしては、

  • 「環境に関わらず安定ソートである」と自分で保証したい
  • ついでに「非破壊(元の配列を壊さない)」にしたい

という理由で、「安定ソートユーティリティ」を用意しておく価値があります。


基本形:インデックスを持たせて安定ソートを実現する

アイデアの核心

安定ソートを自前で保証する一番シンプルなテクニックは、

「元のインデックスを一緒に持たせてソートし、比較で同値だったらインデックスで決める」

というやり方です。

流れを言葉で書くと、

  1. 各要素に「元のインデックス」をくっつけた一時配列を作る
  2. ソートするとき、まず「ソートキー」で比較する
  3. ソートキーが同じなら、「元のインデックス」で比較する(小さいほうを先に)
  4. 最後に元の要素だけを取り出す

こうすると、「同じキー同士は元の順番のまま」になるので、安定ソートが保証されます。

安定ソートユーティリティ 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

要件:

  • ステータスの優先度は tododoingdone の順
  • 同じステータスの中では、作成日時の昇順(古い順)

比較関数を定義する

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 する」という感覚を、ここで一度しっかり掴んでおくと、この先の配列処理が一段レベルアップします。

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