シェルソートアルゴリズム(例付き)

⚡ スマートサマリー

シェルソートは、離れた位置にある要素を比較し、隣接する要素がソートされるまでその間隔を縮小していくことで挿入ソートを一般化した、インプレース比較アルゴリズムです。

  • 📊 定義: 1959年にドナルド・シェルによって提案された、減少するギャップシーケンスを使用する挿入ソートのインプレース一般化。
  • 🔀 ギャップシーケンス: シェルのオリジナルは n/2, n/4, …, 1 ですが、クヌース、セジウィック、シウラの数列の方が実際には優れた性能を発揮します。
  • 複雑: 最良の場合O(n log n)、最悪の場合O(n^2)、補助空間はO(1)。
  • 使用事例: Linuxカーネル、uClibc、およびbzip2は、再帰と余分なスタックメモリを回避するためにシェルソートを使用しています。
  • 🤖 AIの視点: AIアシスタントは、ギャップシーケンスを提案したり、必要に応じてアニメーションによるシェルソートの視覚化を生成したりできます。

シェルソートとは何ですか?

シェルソート(シェル法とも呼ばれる)は、効率的なインプレース比較ベースのソートアルゴリズムである。1959年にこのアイデアを提唱したドナルド・シェルにちなんで名付けられたこのアルゴリズムは、挿入ソートの一般化拡張であり、散在データに対する二次的な計算量の問題を克服している。

基本的な考え方は、離れた要素をグループ化し、挿入ソートを用いて各グループをソートし、ギャップを段階的に縮小していき、最終的に1に近づけるというものです。そうすれば、配列はほぼソートされた状態になります。

このギャップ、つまり間隔は、シェルのオリジナル、クヌース、ヒバード、セジウィックなどの選択されたシーケンスに従います。シェルのオリジナルは n/2, n/4, ..., 1.

シェルソートアルゴリズム

ステップ1) 配列のサイズをnとしたとき、区間値hをh = n/2で初期化します。

ステップ2) 区間hからの距離内にあるすべての要素をサブリストに格納します。

ステップ3) 各サブリストを挿入ソートを使用してソートします。

ステップ4) 新しい区間 h = h/2 を設定します。

ステップ5) h > 0 の場合は、ステップ 2 に戻ります。そうでない場合は、ステップ 6 に進みます。

ステップ6) 結果として得られた配列は完全にソートされています。

シェルソートの仕組み

挿入ソートでは、要素は一度に1つずつしか移動しません。一方、シェルソートは、配列を間隔に基づいて広く間隔を空けたサブリストに分割し、各サブリストに対して挿入ソートを実行します。

間隔が小さくなるにつれて、サブリストのサイズは大きくなります。以前のパスでデータが部分的にソートされているため、間隔が小さいほど、実行時よりもはるかに少ないスワップで済みます。 挿入ソート ゼロから始めます。下の図は、シェルソートの1回のパスを示しています。

シェルソートワークス

シェルソートアルゴリズムの動作例

以下の配列をシェルソートを使ってソートしてみましょう。

シェルソートアルゴリズムの仕組み

ステップ1) 配列のサイズは8なので、初期区間値はh = 8/2 = 4です。

ステップ2) 4文字離れた要素をグループ化します。サブリスト: {8, 1}, {6, 4}, {7, 5}, {2, 3}。

シェルソートアルゴリズムの仕組み

ステップ3) 各サブリストを挿入ソートでソートします。要素が移動する間、一時変数に挿入される値を保持します。スワップ後、配列は次のようになります。

シェルソートアルゴリズムの仕組み

ステップ4) 間隔を狭めます。新しい間隔は h = 4/2 = 2 です。

ステップ5) 2 > 0 なので、ステップ 2 に戻り、2 つ離れた要素をグループ化します: {1, 5, 8, 7} と {4, 2, 6, 3}。

シェルソートアルゴリズムの仕組み

最初のサブリストをソートします。配列は次のようになります。

シェルソートアルゴリズムの仕組み

2番目のサブリストをソートした後:

シェルソートアルゴリズムの仕組み

間隔を再び縮小して h = 2/2 = 1 にします。間隔が 1 の場合、シェルソートは以下に示すように、配列全体に対して最終的な挿入ソート処理を実行します。

シェルソートアルゴリズムの仕組み

シェルソートアルゴリズムの仕組み

シェルソートアルゴリズムの仕組み

ステップ6) 区間を再度分割すると0になります。これで配列は完全にソートされました。

シェルソートアルゴリズムの仕組み

擬似-Code シェルソート用

Start
Input array a of size n
for (interval = n / 2; interval > 0; interval /= 2)
    for (i = interval; i < n; i += 1)
        temp = a[i];
        for (j = i; j >= interval && a[j - interval] > temp; j -= interval)
            a[j] = a[j - interval];
        a[j] = temp;
End

C言語のシェルソートプログラム/C++

入力:

//Shell Sort Program in C/C++
#include <bits/stdc++.h>
using namespace std;
void ShellSort(int data[], int size) {
    for (int interval = size / 2; interval > 0; interval /= 2) {
        for (int i = interval; i < size; i += 1) {
            int temp = data[i];
            int j;
            for (j = i; j >= interval && data[j - interval] > temp; j -= interval) {
                data[j] = data[j - interval];
            }
            data[j] = temp;
        }
    }
}
int main() {
    int data[] = {8, 6, 7, 2, 1, 4, 5, 3};
    int size = sizeof(data) / sizeof(data[0]);
    ShellSort(data, size);
    cout << "Sorted Output: \n";
    for (int i = 0; i < size; i++)
        cout << data[i] << " ";
    cout << "\n";
}

出力:

Sorted Output:

1 2 3 4 5 6 7 8

シェルソートの例 Python

入力:

#Shell Sort Example in Python
def ShellSort(data, size):
    interval = size // 2
    while interval > 0:
        for i in range(interval, size):
            temp = data[i]
            j = i
            while j >= interval and data[j - interval] > temp:
                data[j] = data[j - interval]
                j -= interval
            data[j] = temp
        interval //= 2
data = [8, 6, 7, 2, 1, 4, 5, 3]
ShellSort(data, len(data))
print('Sorted Output:')
print(data)

出力:

Sorted Output:
[1, 2, 3, 4, 5, 6, 7, 8]

シェルソートの応用例

シェルソートは、スタック容量やシンプルさが重要な現代のシステムでも依然として使用されています。

  • その Linuxカーネル 呼び出しスタックを回避することが重要な箇所では、シェルソートを使用します。
  • uClibc組み込みCライブラリは、メモリ使用量を抑えるためにシェルソートを使用しています。
  • bzip2は、ブロックソート中の深い再帰を回避するためにシェルソートを使用します。
  • 組み込みファームウェアは、再帰が制限される小規模なデータセットに対して、シェルソートを優先的に使用します。

シェルソートの利点と欠点

優位性 デメリット
コールスタックは不要なので、組み込みシステムには最適です。 非常に大きな配列の場合、最速の選択肢ではありません。
少量のコードで簡単に実装できます。 要素が広範囲に分散しているデータでは、パフォーマンスが低下します。
中規模または部分的にソートされた配列に対して効率的です。 最悪の場合の時間計算量は、選択されたギャップシーケンスに大きく左右される。
インプレース方式なので、補助メモリを常に一定量使用します。 これは安定ソートではないため、同じキーを持つキーの相対的な順序が変わる可能性があります。

シェルソートの複雑性分析

シェルソートの時間計算量

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

最良の場合、配列がほぼ整列しているときは、各パスに必要なテストの回数は対数で済み、O(n log n) となります。

最悪の場合、配列は要素が最大の比較を必要とするように配置され、最後のインクリメントはシェルの元のシーケンスでO(n^2)で支配的になります。

  1. 最良の場合の計算量:O(n log n)
  2. 平均的なケースの複雑さ:ギャップシーケンスに応じてO(n log n)からO(n^(4/3))
  3. 最悪の場合の計算量:シェルの元のシーケンスではO(n^2)

汎用性の高い最適なギャップ配列は依然として未解決の研究課題であるが、セジウィック配列とシウラ配列は実際には良好な性能を発揮する。

シェルソートの空間計算量

シェルソートは補助配列を必要としないため、入力サイズに関係なく空間計算量はO(1)となり、これはシェルソートの最も強力な実用的な利点の1つです。

よくあるご質問

シェルソートは、1959年にドナルド・シェルによって提案された、インプレース比較ソートアルゴリズムです。これは挿入ソートを一般化したもので、離れた位置にある要素を比較し、隣接する要素がソートされるまでその間隔を縮小していくことで、スワップの回数を大幅に削減します。

シェルのオリジナルシーケンスを用いた場合、最良の場合の時間計算量はO(n log n)、最悪の場合の時間計算量はO(n^2)です。セジウィックのシーケンスなど、より優れたギャップシーケンスを用いると、最悪の場合の時間計算量は約O(n^(4/3))にまで減少します。空間計算量はO(1)です。

いいえ、シェルソートは安定していません。要素が大きな間隔を置いて比較および交換されるため、同じキーを持つ2つの要素の相対的な順序が、処理中に変化する可能性があります。安定性が重要な場合は、マージソートまたは挿入ソートの安定版を使用してください。

挿入ソートは要素を一度に1つずつ移動させます。シェルソートはまず離れた位置にある要素を比較し、徐々にその間隔を狭めていきます。その結果、間隔が1になる頃にはほぼソートされた配列になっているため、最後の挿入ソート処理は非常に速く完了します。

AIアシスタントは、データセットのサイズ、分布、制約を分析し、シェルソート、クイックソート、基数ソートなどのアルゴリズムを推奨します。また、実行時間とメモリ使用量を比較するベンチマークスクリプトを生成することもできるため、実際のワークロードで推奨アルゴリズムを検証できます。

はい。AIツールを使えば、シェルソートのアニメーションによる視覚化を生成し、ギャップグループ、比較、スワップをリアルタイムで強調表示できます。このような視覚化は、学習者が間隔がどのように縮小していくか、そして配列がパスを重ねるごとにどのようにソートされた状態に収束していくかを理解する上で役立ちます。