マージソートは、分割統治法(divide and conquer)を用いた効率的なソートアルゴリズムです。以下にマージソートの特徴と仕組みを解説します。

1. マージソートの基本概念

マージソートは以下の3つのステップで動作します:

  1. 分割(Divide): 配列を半分に分割します
  2. 統治(Conquer): 分割した各部分を再帰的にソートします
  3. 結合(Merge): ソートされた部分配列を結合して完全にソートされた配列を作ります

もう少しわかりやすく整理すると次のようになります。

マージソートは大きく「分割(Divide)」と「統治・マージ(Conquer & Merge)」の2つのフェーズに分けられ、それぞれが再帰的に処理されます。

  1. 分割(Divide)フェーズ:
    • 配列を単一要素(長さ1)になるまで再帰的に分割していきます
    • これは「分割→分割→分割…」と木構造のように下へ下へと進みます
    • 単一要素になった時点で、その要素は「すでにソート済み」とみなされます
  2. 統治・マージ(Conquer & Merge)フェーズ:
    • 単一要素になった後、各部分配列をマージ(結合)していきます
    • マージする際に、2つのソート済み配列を比較しながら新しいソート済み配列を作ります
    • これを再帰呼び出しから戻りながら繰り返し、最終的に完全にソートされた1つの配列になります

2. マージソートのアルゴリズム

関数 マージソート(配列):
    もし 配列の長さ ≤ 1 なら:
        配列を返す(すでにソート済みとみなす)
    
    中間点 = 配列の長さ ÷ 2
    左側 = マージソート(配列の前半)
    右側 = マージソート(配列の後半)
    
    結合(左側, 右側)を返す

関数 結合(左側配列, 右側配列):
    結果配列 = 空の配列
    左側インデックス = 0
    右側インデックス = 0
    
    while 左側インデックス < 左側配列の長さ かつ 右側インデックス < 右側配列の長さ:
        if 左側配列[左側インデックス] <= 右側配列[右側インデックス]:
            結果配列に左側配列[左側インデックス]を追加
            左側インデックスを1増やす
        else:
            結果配列に右側配列[右側インデックス]を追加
            右側インデックスを1増やす
    
    残りの要素を結果配列に追加
    結果配列を返す

はい

いいえ

配列の入力

配列の長さ <= 1?

そのまま返す

単一要素は既にソート済み

配列を半分に分割

左半分を取得

右半分を取得

左半分に対して

マージソートを再帰的に適用

右半分に対して

マージソートを再帰的に適用

ソートされた左半分と右半分をマージ

ソートされた配列を返す

マージ処理

はい

いいえ

はい

いいえ

左右の配列の先頭要素を比較

左の要素 <= 右の要素?

左の要素を結果配列に追加

左配列のインデックスを進める

右の要素を結果配列に追加

右配列のインデックスを進める

両方の配列に要素が残っている?

残りの要素を全て結果配列に追加

結果配列を返す

より具体的な例は以下のとおりです。

マージフェーズ (Merge)

分割フェーズ (Divide)

入力配列: [38, 27, 43, 3, 9, 82, 10]

分割: [38, 27, 43, 3] と [9, 82, 10]

分割: [38, 27] と [43, 3]

分割: [9, 82] と [10]

分割: [38] と [27]

分割: [43] と [3]

分割: [9] と [82]

[10](単一要素)

マージ: [27, 38]

マージ: [3, 43]

マージ: [9, 82]

マージ: [3, 27, 38, 43]

マージ: [9, 10, 82]

最終マージ: [3, 9, 10, 27, 38, 43, 82]

3. マージソートの実装例(JavaScript)

より具体的な理解のために、JavaScriptでの実装例を示します:

function mergeSort(arr) {
  // 配列の長さが1以下の場合はそのまま返す
  if (arr.length <= 1) {
    return arr;
  }
  
  // 配列を半分に分割
  const middle = Math.floor(arr.length / 2);
  const left = arr.slice(0, middle);
  const right = arr.slice(middle);
  
  // 分割した配列を再帰的にソート
  return merge(
    mergeSort(left),
    mergeSort(right)
  );
}

function merge(left, right) {
  let result = [];
  let leftIndex = 0;
  let rightIndex = 0;
  
  // 左右の配列を比較しながら小さい方から結果配列に追加
  while (leftIndex < left.length && rightIndex < right.length) {
    if (left[leftIndex] < right[rightIndex]) {
      result.push(left[leftIndex]);
      leftIndex++;
    } else {
      result.push(right[rightIndex]);
      rightIndex++;
    }
  }
  
  // 残りの要素を追加
  return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
}

4. マージソートのシミュレーション

5. マージソートの特徴

  • 時間計算量: O(n log n) – これは配列のサイズに関係なく常に保証されます
  • 空間計算量: O(n) – 追加のメモリ空間が必要です
  • 安定性: 安定ソート(同じ値の要素の相対的な順序が保持される)
  • 適用場合: 大規模データや連結リストのソートに適しています

6. マージソートの利点と欠点

6.1. 利点

  • 常に O(n log n) の時間計算量を保証
  • 安定ソートである
  • 外部ソートに適している(全データをメモリに読み込まなくても処理可能)

6.2. 欠点

  • 追加のメモリ空間が必要
  • 小さな配列に対しては挿入ソートなどの単純なアルゴリズムの方が効率的な場合がある

マージソートはその安定性と予測可能な性能から、多くの言語の標準ライブラリでも採用されているソートアルゴリズムの一つです。大きなデータセットや連結リストのソートに特に有効です。

7. その他のソート