シェルソート(Shell Sort)

1. シェルソートとは何か

 シェルソート(Shell Sort)は、1959年にドナルド・シェル(Donald Shell)によって開発された比較ソートアルゴリズムです。このアルゴリズムは挿入ソートを改良したもので、「減少増分ソート」(diminishing increment sort)とも呼ばれています。

 シェルソートの基本的なアイデアは非常にシンプルです:

  1. 配列を一定の「ギャップ(間隔)」ごとに分けられた複数のサブ配列に分割する
  2. 各サブ配列に対して挿入ソートを適用する
  3. ギャップを徐々に小さくしながら、上記のプロセスを繰り返す
  4. 最終的にギャップが1になったとき、通常の挿入ソートを行う

 このアプローチにより、挿入ソートの主な弱点である「小さな値が配列の末尾にある場合、それを先頭まで移動するのに多くの交換が必要になる」という問題を解決します。シェルソートでは大きなギャップでの初期ソートにより、要素を「おおよそ」正しい位置に素早く移動させます。

2. シェルソートの動作原理

基本ステップ

  1. ギャップシーケンスの選択: 初期ギャップを選び、徐々に減少させるシーケンスを決定する
  2. ギャップソート: 現在のギャップで配列を複数のサブグループに分け、各サブグループ内で挿入ソートを行う
  3. ギャップの減少: ギャップを小さくして、再度ステップ2を実行する
  4. 最終処理: ギャップが1になったら、通常の挿入ソートで配列全体をソートする

動作例

 配列 [9, 8, 3, 7, 5, 6, 4, 1] をシェルソートでソートする例を見てみましょう。 ギャップシーケンスとして [4, 2, 1] を使用します。

ギャップ = 4:

 まず、間隔4の要素同士でサブ配列を作ります:

  • サブ配列1: [9, 5] → ソート後 [5, 9]
  • サブ配列2: [8, 6] → ソート後 [6, 8]
  • サブ配列3: [3, 4] → ソート後 [3, 4]
  • サブ配列4: [7, 1] → ソート後 [1, 7]

 ギャップ4でのソート後の配列: [5, 6, 3, 1, 9, 8, 4, 7]

ギャップ = 2:

 間隔2の要素同士でサブ配列を作ります:

  • サブ配列1: [5, 3, 9, 4] → ソート後 [3, 4, 5, 9]
  • サブ配列2: [6, 1, 8, 7] → ソート後 [1, 6, 7, 8]

 ギャップ2でのソート後の配列: [3, 1, 4, 6, 5, 7, 9, 8]

ギャップ = 1:

 通常の挿入ソートを行います。 最終的なソート後の配列: [1, 3, 4, 5, 6, 7, 8, 9]

3. ギャップシーケンスの選択

 シェルソートの性能は使用するギャップシーケンスに大きく依存します。一般的に使われるギャップシーケンスには以下のようなものがあります:

1. シェルの元々のシーケンス

N/2, N/4, N/8, …, 1(Nは配列の長さ)

例: N=16の場合、[8, 4, 2, 1]

2. Knuthのシーケンス

(3^k – 1)/2, …, 40, 13, 4, 1(最大値 < N/3)

例: N=100の場合、[40, 13, 4, 1]

3. Sedgewickのシーケンス

4^i + 32^(i-1) + 1 および 9(4^i – 2^i) + 1

例: [1, 5, 19, 41, 109, ...]

4. Ciuraのシーケンス

実験的に導き出された最適なシーケンス

[1, 4, 10, 23, 57, 132, 301, 701, 1750, ...]

異なるギャップシーケンスを使用することで、シェルソートの時間計算量が変わります。最も効率的なシーケンスを使用した場合、シェルソートはO(N log²N)の時間計算量を達成できます。

4. シェルソートのシミュレーション

5. シェルソートの実装

JavaScript でのシェルソートの実装例:

function shellSort(arr) {
  const n = arr.length;
  
  // Knuthのシーケンスを生成
  let gap = 1;
  while (gap < n / 3) {
    gap = gap * 3 + 1;
  }
  
  // ギャップを徐々に減少させながらソート
  while (gap > 0) {
    // 各ギャップでの挿入ソート
    for (let i = gap; i < n; i++) {
      const temp = arr[i];
      let j = i;
      
      // サブ配列内の要素を比較し、必要に応じて入れ替え
      while (j >= gap && arr[j - gap] > temp) {
        arr[j] = arr[j - gap];
        j -= gap;
      }
      
      arr[j] = temp;
    }
    
    // ギャップを縮小(Knuthのシーケンスの場合)
    gap = Math.floor(gap / 3);
  }
  
  return arr;
}

6. シェルソートの特徴

長所

  1. 挿入ソートの改良版: 挿入ソートの欠点を克服しつつ、そのシンプルさを保持しています
  2. 適応的: 部分的にソート済みの配列に対して効率的に動作します
  3. インプレース: 追加のメモリ空間をほとんど必要としません
  4. 安定ではないが効率的: クイックソートのような高度なアルゴリズムほど複雑ではありませんが、単純なO(N²)アルゴリズムよりも効率的です

短所

  1. 安定ではない: 等しい要素の相対的な順序が保存されない場合があります
  2. ギャップシーケンスに依存: 使用するギャップシーケンスによって性能が大きく変わります
  3. 最適なギャップシーケンス不明: 理論的に最適なギャップシーケンスはまだ発見されていません

7. 時間計算量

 シェルソートの時間計算量は使用するギャップシーケンスに依存します:

  • 最悪の場合: O(N²)(シェルの元々のシーケンスを使用)
  • 平均的な場合: O(N^1.25)(Knuthのシーケンスを使用)
  • 最良の場合: O(N log N)(すでにソートされている配列)

 現在知られている最も効率的なギャップシーケンスを使用した場合、シェルソートの時間計算量はO(N log²N)程度と考えられています。

8. シェルソートの応用

 シェルソートは以下のような場面で特に有用です:

  1. 組み込みシステム: 実装がシンプルで効率的なため、メモリやプロセッサに制約のある環境に適しています
  2. 中規模データセット: 数百から数千要素の配列に対して効率的に動作します
  3. ほぼソート済みのデータ: 既にある程度整列しているデータに対して特に効率的です
  4. ハイブリッドソートアルゴリズム: 他のソートアルゴリズムの一部として使用されることがあります

9. 他のソートアルゴリズムとの比較

アルゴリズム最良の場合平均的な場合最悪の場合空間計算量安定性
シェルソートO(N log N)O(N log² N)O(N²)O(1)不安定
挿入ソートO(N)O(N²)O(N²)O(1)安定
クイックソートO(N log N)O(N log N)O(N²)O(log N)不安定
マージソートO(N log N)O(N log N)O(N log N)O(N)安定
ヒープソートO(N log N)O(N log N)O(N log N)O(1)不安定

 シェルソートは、挿入ソートよりも効率的でありながら、クイックソートやマージソートほど複雑ではありません。メモリ使用量が少なく、実装も比較的シンプルなため、特定の状況では非常に実用的なアルゴリズムです。

10. まとめ

 シェルソートは、単純な挿入ソートのアイデアを拡張した効率的なソートアルゴリズムです。ギャップシーケンスを使用して要素を「荒くソート」してから徐々に細かくソートしていくという独特のアプローチにより、挿入ソートよりも大幅に性能が向上します。

 実装のシンプルさ、メモリ効率の良さ、そして比較的良好な性能により、シェルソートは今でも特定の状況で使用される価値のあるアルゴリズムです。特に、組み込みシステムや中規模データセットに対して、複雑なアルゴリズムを使用するほどではないが単純なO(N²)ソートよりは効率的なアルゴリズムが必要な場合に適しています。

 ギャップシーケンスの選択がシェルソートの性能に大きく影響するため、使用するデータの特性に合わせて適切なシーケンスを選ぶことが重要です。

11. その他のソート

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