1. シェルソートとは何か
シェルソート(Shell Sort)は、1959年にドナルド・シェル(Donald Shell)によって開発された比較ソートアルゴリズムです。このアルゴリズムは挿入ソートを改良したもので、「減少増分ソート」(diminishing increment sort)とも呼ばれています。
シェルソートの基本的なアイデアは非常にシンプルです:
- 配列を一定の「ギャップ(間隔)」ごとに分けられた複数のサブ配列に分割する
- 各サブ配列に対して挿入ソートを適用する
- ギャップを徐々に小さくしながら、上記のプロセスを繰り返す
- 最終的にギャップが1になったとき、通常の挿入ソートを行う
このアプローチにより、挿入ソートの主な弱点である「小さな値が配列の末尾にある場合、それを先頭まで移動するのに多くの交換が必要になる」という問題を解決します。シェルソートでは大きなギャップでの初期ソートにより、要素を「おおよそ」正しい位置に素早く移動させます。
2. シェルソートの動作原理
基本ステップ
- ギャップシーケンスの選択: 初期ギャップを選び、徐々に減少させるシーケンスを決定する
- ギャップソート: 現在のギャップで配列を複数のサブグループに分け、各サブグループ内で挿入ソートを行う
- ギャップの減少: ギャップを小さくして、再度ステップ2を実行する
- 最終処理: ギャップが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. シェルソートの特徴
長所
- 挿入ソートの改良版: 挿入ソートの欠点を克服しつつ、そのシンプルさを保持しています
- 適応的: 部分的にソート済みの配列に対して効率的に動作します
- インプレース: 追加のメモリ空間をほとんど必要としません
- 安定ではないが効率的: クイックソートのような高度なアルゴリズムほど複雑ではありませんが、単純なO(N²)アルゴリズムよりも効率的です
短所
- 安定ではない: 等しい要素の相対的な順序が保存されない場合があります
- ギャップシーケンスに依存: 使用するギャップシーケンスによって性能が大きく変わります
- 最適なギャップシーケンス不明: 理論的に最適なギャップシーケンスはまだ発見されていません
7. 時間計算量
シェルソートの時間計算量は使用するギャップシーケンスに依存します:
- 最悪の場合: O(N²)(シェルの元々のシーケンスを使用)
- 平均的な場合: O(N^1.25)(Knuthのシーケンスを使用)
- 最良の場合: O(N log N)(すでにソートされている配列)
現在知られている最も効率的なギャップシーケンスを使用した場合、シェルソートの時間計算量はO(N log²N)程度と考えられています。
8. シェルソートの応用
シェルソートは以下のような場面で特に有用です:
- 組み込みシステム: 実装がシンプルで効率的なため、メモリやプロセッサに制約のある環境に適しています
- 中規模データセット: 数百から数千要素の配列に対して効率的に動作します
- ほぼソート済みのデータ: 既にある程度整列しているデータに対して特に効率的です
- ハイブリッドソートアルゴリズム: 他のソートアルゴリズムの一部として使用されることがあります
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. その他のソート





