はい、承知いたしました。ソートアルゴリズムの種類、計算量、安定性を比較する詳細な記事を作成します。約5000語のボリュームを目指し、各アルゴリズムについて丁寧に説明いたします。
ソートアルゴリズムの種類まとめ:計算量と安定性を徹底比較
はじめに:なぜソートは重要なのか?
コンピュータサイエンスの世界において、ソート(整列)は最も基本的な処理の一つです。データを特定の順序(昇順または降順)に並べ替えることで、その後のデータ処理や分析の効率を劇的に向上させることができます。例えば、大量のデータから特定の要素を探す「検索」処理を考えてみましょう。もしデータがソートされていれば、二分探索のような非常に高速なアルゴリズムを利用できますが、ソートされていなければ、全ての要素を一つずつ確認する線形探索を行うしかなく、検索に要する時間は格段に増大します。
データベースのインデックス作成、ファイルシステムの管理、グラフィック処理、機械学習アルゴリズムの準備など、ソートは私たちの身の回りの多くの技術において、性能の基盤を支える役割を担っています。
しかし、一口に「ソート」といっても、その実現方法は一つではありません。データを並べ替えるためのアルゴリズムは数多く存在し、それぞれに異なる特性を持っています。どのアルゴリズムを選択するかは、対象となるデータの規模、データの特性(既にどの程度ソートされているか、値の範囲など)、要求される処理速度、使用できるメモリ容量、そして「安定性」と呼ばれる特殊な要件など、様々な要因によって決定されます。
この記事では、代表的なソートアルゴリズムを網羅的に紹介し、それぞれの「計算量」(処理速度とメモリ使用量)と「安定性」に焦点を当てて詳細に解説します。各アルゴリズムの考え方、具体的な手順、そしてどのような場面に適しているのかを深く理解することで、あなたのプログラムにおいて最適なソート手法を選択できるようになることを目指します。
ソートアルゴリズムを評価する基準
ソートアルゴリズムを比較・評価する上で、いくつかの重要な基準があります。これらを理解することが、各アルゴリズムの特性を把握する上で不可欠です。
1. 計算量 (Complexity)
計算量とは、アルゴリズムが問題を解くために必要とする資源(時間やメモリ)の量を、入力のサイズ(通常はデータの要素数 N)の関数として表現したものです。主に「時間計算量」と「空間計算量」があります。
-
時間計算量 (Time Complexity):
アルゴリズムの実行にかかる時間を示す指標です。入力サイズ N が大きくなったときに、実行時間がどの程度増加するかを評価します。通常、最も重要な操作(比較や交換など)の回数を数えることで評価されます。時間計算量は、最良ケース(Best Case)、平均ケース(Average Case)、最悪ケース(Worst Case)の3つのシナリオで評価されることが多いです。これは、入力データの初期状態によって実行時間が大きく変わるアルゴリズムがあるためです。
計算量は「O記法(オーダー記法)」を用いて表現されます。例えば O(N^2) は、入力サイズ N の二乗に比例して実行時間が増加することを示します。O(N log N) は、N log N に比例して実行時間が増加することを示し、一般に O(N^2) より高速です。O(N) は、N に比例して実行時間が増加する、非常に効率の良い状態を示します。理想は O(1)(入力サイズに関係なく一定時間)ですが、ソートにおいては少なくとも全ての要素を一度は見る必要があるため、理論的な最小時間計算量は比較ベースのソートで O(N log N)、非比較ベースのソートで O(N) となります。 -
空間計算量 (Space Complexity):
アルゴリズムの実行中に必要とするメモリ(記憶領域)の量を示す指標です。これも入力サイズ N の関数として評価されます。補助的に必要とするメモリの量に注目することが多く、入力データ自体が占めるメモリは含めないことが一般的です。空間計算量が O(1) であるアルゴリズムは、「in-place(インプレース)」ソートと呼ばれ、入力配列内で要素の交換などを行い、ほとんど追加のメモリを必要としません。これは、メモリが限られている環境では重要な特性となります。
2. 安定性 (Stability)
安定性とは、ソート対象の配列内に同じ値を持つ複数の要素が存在する場合に、それらの要素の相対的な順序がソート後も維持されるかどうかを示す特性です。
例えば、以下のようなデータがあったとします。
[(値: 5, 色: 赤), (値: 3, 色: 青), (値: 5, 色: 緑)]
これを値の昇順でソートする場合、結果は以下のようになるはずです。
[(値: 3, 色: 青), (値: 5, 色: …), (値: 5, 色: …)]
このとき、元の配列では「(値: 5, 色: 赤)」が「(値: 5, 色: 緑)」より前にありました。ソート後の配列で、この相対的な順序(「赤の5」が「緑の5」より前に来る)が保たれていれば、そのソートアルゴリズムは「安定」であるといえます。そうでなければ「不安定」です。
安定性が重要なのは、複数のキー(例えば、まず部署でソートし、次に同じ部署内の社員を年齢でソートする)でソートを行う場合などです。不安定なソートで年齢ソートを行うと、先に部署でソートした順序が崩れてしまう可能性があります。
3. in-place (インプレース)
空間計算量と関連しますが、in-placeソートは、ソートのために必要となる追加のメモリ領域が、入力配列のサイズに対して定数(O(1))であるアルゴリズムを指します。つまり、入力配列自体を書き換えることでソートを行います。これはメモリ効率の観点から重要です。対照的に、O(N) 以上の追加メモリを必要とするアルゴリズム(例えば、ソート済みの要素を格納するための一時配列を作成するなど)は、in-placeではないとされます。
これらの基準を基に、様々なソートアルゴリズムを見ていきましょう。
主要なソートアルゴリズムの詳細
ここでは、代表的なソートアルゴリズムについて、その仕組み、手順、計算量、安定性、そして特徴を詳しく解説します。
1. バブルソート (Bubble Sort)
-
概要:
バブルソートは、最も単純なソートアルゴリズムの一つです。配列を繰り返し走査し、隣り合う要素の順序が逆であれば交換するという操作を、配列全体がソートされるまで繰り返します。まるで泡(バブル)が水面に浮き上がるように、最も大きい(あるいは小さい)要素が端に移動していくことからこの名が付きました。 -
手順:
- 配列の先頭から末尾に向かって走査を開始します。
- 現在の要素とその次の要素を比較します。
- もし現在の要素が次の要素より大きい(昇順の場合)ならば、二つの要素を交換します。
- 配列の末尾までこれを繰り返します。この一度の走査で、最も大きい要素が配列の末尾(まだソートされていない部分の末尾)に移動します。
- 上記の走査を、ソート済み領域を除いた残りの配列に対して繰り返します。これを配列全体がソートされるまで(つまり、一度の走査で全く交換が行われなくなるまで)続けます。
-
例: 配列 [5, 1, 4, 2, 8] を昇順にソートする
- 1回目走査:
- [5, 1, 4, 2, 8] -> 5と1を比較、交換 -> [1, 5, 4, 2, 8]
- [1, 5, 4, 2, 8] -> 5と4を比較、交換 -> [1, 4, 5, 2, 8]
- [1, 4, 5, 2, 8] -> 5と2を比較、交換 -> [1, 4, 2, 5, 8]
- [1, 4, 2, 5, 8] -> 5と8を比較、交換なし -> [1, 4, 2, 5, 8]
- (末尾の8は最大なので確定)
- 2回目走査 (対象: [1, 4, 2, 5]):
- [1, 4, 2, 5] -> 1と4を比較、交換なし -> [1, 4, 2, 5]
- [1, 4, 2, 5] -> 4と2を比較、交換 -> [1, 2, 4, 5]
- [1, 2, 4, 5] -> 4と5を比較、交換なし -> [1, 2, 4, 5]
- (末尾の5は確定)
- 3回目走査 (対象: [1, 2, 4]):
- [1, 2, 4] -> 1と2を比較、交換なし -> [1, 2, 4]
- [1, 2, 4] -> 2と4を比較、交換なし -> [1, 2, 4]
- (末尾の4は確定)
- 4回目走査 (対象: [1, 2]):
- [1, 2] -> 1と2を比較、交換なし -> [1, 2]
- (交換なし、ソート完了を検出)
最終的なソート済み配列: [1, 2, 4, 5, 8]
- 1回目走査:
-
計算量:
- 時間計算量:
- 最悪ケース: O(N^2) – 配列が完全に逆順になっている場合など、毎回交換が必要な場合。N回の走査のそれぞれで最大N-1回の比較と交換が発生するため。
- 平均ケース: O(N^2) – 多くのランダムな入力に対して。
- 最良ケース: O(N) – 配列が既にソートされている場合。一度の走査で交換が発生しないことを検出して終了できるため。
- 空間計算量: O(1) – 要素の交換のために一時的な変数が必要なだけで、追加のメモリはほとんど不要(in-place)。
- 時間計算量:
-
安定性: 安定 – 同じ値を持つ要素は、隣り合った場合にのみ比較・交換されます。等しい要素同士は順序を保ったまま比較が行われず、交換もされないため、相対的な順序は維持されます。
-
特徴:
- 実装が非常に容易。
- 既にほぼソートされているデータに対しては効率が良い(O(N))。
- 大規模なデータに対しては、時間計算量が O(N^2) であるため実用的ではない。教育目的でアルゴリズムの基礎を学ぶのに適している。
2. 選択ソート (Selection Sort)
-
概要:
選択ソートは、未ソート部分から最小(または最大)の要素を見つけ出し、それを未ソート部分の先頭の要素と交換するという操作を繰り返すアルゴリズムです。一度の交換で、一つの要素が最終的な位置に確定します。 -
手順:
- 配列全体を未ソート部分とします。
- 未ソート部分の中から最小の要素を探し出します。
- 見つけ出した最小の要素と、未ソート部分の先頭の要素を交換します。これにより、未ソート部分の先頭の要素がソート済み領域の末尾として確定します。
- 未ソート部分のサイズを1減らし、配列全体がソートされるまで手順2と3を繰り返します。
-
例: 配列 [5, 1, 4, 2, 8] を昇順にソートする
- 未ソート部分: [5, 1, 4, 2, 8]
- 最小値は1。位置0の要素5と交換。
- 配列: [1, 5, 4, 2, 8]。ソート済み: [1]
- 未ソート部分: [5, 4, 2, 8] (位置1から)
- 最小値は2。位置1の要素5と交換。
- 配列: [1, 2, 4, 5, 8]。ソート済み: [1, 2]
- 未ソート部分: [4, 5, 8] (位置2から)
- 最小値は4。位置2の要素4と交換 (交換なし)。
- 配列: [1, 2, 4, 5, 8]。ソート済み: [1, 2, 4]
- 未ソート部分: [5, 8] (位置3から)
- 最小値は5。位置3の要素5と交換 (交換なし)。
- 配列: [1, 2, 4, 5, 8]。ソート済み: [1, 2, 4, 5]
- 未ソート部分: [8] (位置4から)
- 最小値は8。位置4の要素8と交換 (交換なし)。
- 配列: [1, 2, 4, 5, 8]。ソート済み: [1, 2, 4, 5, 8]
最終的なソート済み配列: [1, 2, 4, 5, 8]
- 未ソート部分: [5, 1, 4, 2, 8]
-
計算量:
- 時間計算量:
- 最悪ケース: O(N^2) – 1回目の走査でN-1回の比較、2回目でN-2回…合計 N(N-1)/2 回の比較が必要。交換回数は最大N-1回。比較回数が支配的。
- 平均ケース: O(N^2) – 常にN(N-1)/2 回の比較が必要。
- 最良ケース: O(N^2) – 既にソートされていても、最小値を探すための比較は省略できないため。
- 空間計算量: O(1) – 要素の交換のために一時的な変数が必要なだけで、追加のメモリはほとんど不要(in-place)。
- 時間計算量:
-
安定性: 不安定 – 最小値を見つけて未ソート部分の先頭要素と交換する際、同じ値を持つ要素の相対的な順序が崩れる可能性があります。例えば、[5a, 5b, 1] をソートする場合、最小値1を先頭の5aと交換すると [1, 5b, 5a] となり、元の5a, 5bの順序が逆転します。
-
特徴:
- 実装が比較的容易。
- 比較回数は常に O(N^2) ですが、交換回数が最大 N-1 回と少ない。要素の移動コストが高い場合に有利な場合がある。
- バブルソートと同様、大規模なデータに対しては実用的ではない。時間計算量は入力の初期状態に依存しない。
3. 挿入ソート (Insertion Sort)
-
概要:
挿入ソートは、配列を「ソート済み部分」と「未ソート部分」に分け、未ソート部分から一つずつ要素を取り出し、それをソート済み部分の適切な位置に挿入していくアルゴリズムです。ちょうど、トランプの手札を整理するときのように、新しいカードを既に並べたカードの間に差し込んでいくイメージです。 -
手順:
- 配列の最初の要素をソート済み部分とします(サイズ1)。
- 未ソート部分の最初の要素(配列の2番目の要素から開始)を取り出します。
- 取り出した要素を、ソート済み部分の要素と比較しながら、適切な挿入位置を探します。挿入位置が見つかるまで、ソート済み部分の要素を一つずつ後ろにずらしていきます。
- 適切な位置に要素を挿入します。
- 未ソート部分の要素が全てなくなるまで、手順2から4を繰り返します。
-
例: 配列 [5, 1, 4, 2, 8] を昇順にソートする
- 初期状態: [5, 1, 4, 2, 8] (ソート済み: [5], 未ソート: [1, 4, 2, 8])
- 要素 1 を取り出す。ソート済み部分 [5] に挿入位置を探す。5より小さいので先頭に挿入。
- 配列: [1, 5, 4, 2, 8] (ソート済み: [1, 5], 未ソート: [4, 2, 8])
- 要素 4 を取り出す。ソート済み部分 [1, 5] に挿入位置を探す。1より大きい、5より小さいので、1と5の間に挿入。
- 配列: [1, 4, 5, 2, 8] (ソート済み: [1, 4, 5], 未ソート: [2, 8])
- 要素 2 を取り出す。ソート済み部分 [1, 4, 5] に挿入位置を探す。1より大きい、4より小さいので、1と4の間に挿入。
- 配列: [1, 2, 4, 5, 8] (ソート済み: [1, 2, 4, 5], 未ソート: [8])
- 要素 8 を取り出す。ソート済み部分 [1, 2, 4, 5] に挿入位置を探す。5より大きいので末尾に挿入。
- 配列: [1, 2, 4, 5, 8] (ソート済み: [1, 2, 4, 5, 8], 未ソート: [])
最終的なソート済み配列: [1, 2, 4, 5, 8]
-
計算量:
- 時間計算量:
- 最悪ケース: O(N^2) – 配列が完全に逆順になっている場合。各要素を挿入する際に、ソート済み部分の全ての要素と比較し、ずらす必要があるため。
- 平均ケース: O(N^2) – 多くのランダムな入力に対して。
- 最良ケース: O(N) – 配列が既にソートされている場合。各要素はソート済み部分の末尾と比較するだけで挿入位置が決まるため。
- 空間計算量: O(1) – 要素を一時的に保持する変数が必要なだけで、追加のメモリはほとんど不要(in-place)。
- 時間計算量:
-
安定性: 安定 – 同じ値を持つ要素を挿入する際、ソート済み部分にある同じ値の要素の後ろに挿入することで、相対的な順序を維持できます。
-
特徴:
- 実装が容易。
- データ数が少ない場合や、ほぼソート済みのデータに対して非常に効率が良い(O(N))。
- オンラインソートが可能(データが逐次的に入力される場合でも処理可能)。
- バブルソート、選択ソートと同様、大規模なデータには向かない。しかし、これら3つの中では一般的に挿入ソートが最も高速とされることが多い。
4. マージソート (Merge Sort)
-
概要:
マージソートは、「分割統治法」に基づいたアルゴリズムです。配列を繰り返し半分に分割していき、要素数が1になるまで分割します。その後、分割した小さな配列を、それぞれソートしながら結合(マージ)していきます。ソート済みの2つの配列を結合する操作は、比較的効率よく行えるのが特徴です。 -
手順:
- 分割: 入力配列を中央で二つに分割します。
- 再帰: 分割されたそれぞれの部分配列に対して、マージソートを再帰的に適用します。要素数が1になるまで分割を繰り返します。要素数1の配列はそれ自体がソート済みとみなします。
- 結合 (マージ): 再帰呼び出しから戻ってきたら、ソート済みの二つの部分配列を結合し、一つのソート済み配列を作成します。この結合操作では、二つの部分配列の先頭から小さい方の要素を順に取り出し、新しい配列に格納していきます。どちらかの部分配列が空になったら、残りの要素を全て新しい配列に移します。
-
例: 配列 [5, 1, 4, 2, 8, 3, 7, 6] を昇順にソートする
- 分割: [5, 1, 4, 2], [8, 3, 7, 6]
- 分割: [5, 1], [4, 2], [8, 3], [7, 6]
- 分割: [5], [1], [4], [2], [8], [3], [7], [6] (要素数1で分割終了)
- 分割: [5, 1], [4, 2], [8, 3], [7, 6]
- 結合 (マージ):
- [5], [1] -> マージ -> [1, 5]
- [4], [2] -> マージ -> [2, 4]
- [8], [3] -> マージ -> [3, 8]
- [7], [6] -> マージ -> [6, 7]
- 中間結果: [1, 5], [2, 4], [3, 8], [6, 7]
- 結合 (マージ):
- [1, 5], [2, 4] -> マージ -> [1, 2, 4, 5]
- [3, 8], [6, 7] -> マージ -> [3, 6, 7, 8]
- 中間結果: [1, 2, 4, 5], [3, 6, 7, 8]
- 結合 (マージ):
- [1, 2, 4, 5], [3, 6, 7, 8] -> マージ -> [1, 2, 3, 4, 5, 6, 7, 8]
最終的なソート済み配列: [1, 2, 3, 4, 5, 6, 7, 8]
- 分割: [5, 1, 4, 2], [8, 3, 7, 6]
-
計算量:
- 時間計算量:
- 最悪ケース: O(N log N) – 配列を log N 段階で分割し、各段階で合計 O(N) の比較と要素移動(マージ)が発生するため。
- 平均ケース: O(N log N)
- 最良ケース: O(N log N) – 分割と結合の構造上、入力の初期状態に大きく依存しない。
- 空間計算量: O(N) – 結合(マージ)の際に、一時的に要素を格納するための補助配列が必要となるため。これは一般的に O(N) の空間を必要とします。メモリに制約がある場合は欠点となる可能性があります。ただし、バリエーションによっては O(log N) や O(1) を実現するものもあります(インプレースマージソートなど、ただし複雑で遅くなる傾向)。
- 時間計算量:
-
安定性: 安定 – 結合(マージ)の際、同じ値を持つ要素が現れた場合に、元の配列で先だった方を先に新しい配列に移すようにすることで、相対的な順序を容易に維持できます。
-
特徴:
- 時間計算量が常に O(N log N) であり、大規模なデータに対しても効率的で予測可能な性能を持つ。
- 安定なソートアルゴリズムである。
- 分割統治法に基づいているため、並列処理に適している。
- O(N) の補助メモリが必要となる点が欠点となる場合がある。
5. クイックソート (Quick Sort)
-
概要:
クイックソートもマージソートと同様、「分割統治法」に基づいたアルゴリズムです。配列から「ピボット(基準要素)」を一つ選び、そのピボットを基準に配列を二つの部分配列に分割します。一つはピボットより小さい要素の集まり、もう一つはピボットより大きい要素の集まりです。ピボットは分割後の正しい位置に配置されます。この分割操作を、分割された部分配列に対して再帰的に適用することでソートを完了させます。 -
手順:
- 分割 (パーティショニング): 配列の中からピボット要素を一つ選択します(選び方にはいくつか方法がある。例: 最初/最後/中央の要素、ランダムな要素)。ピボットより小さい要素をピボットの左側に、ピボットより大きい要素をピボットの右側に移動させます。この操作により、ピボットは最終的にソート後の正しい位置に配置されます。
- 再帰: ピボットの左側の部分配列と、右側の部分配列に対して、クイックソートを再帰的に適用します。部分配列のサイズが1以下になったら再帰を停止します。
-
例: 配列 [5, 1, 4, 2, 8, 3, 7, 6] を昇順にソートする (ピボットを右端の要素とする場合)
- 初期配列: [5, 1, 4, 2, 8, 3, 7, 6] (ピボット: 6)
- パーティショニング: 6より小さい要素を左、大きい要素を右に集める。
- [5, 1, 4, 2, 3] | [6] | [8, 7]
- ピボット6は正しい位置に固定される。
- 左側部分配列: [5, 1, 4, 2, 3] (ピボット: 3)
- パーティショニング: 3より小さい要素を左、大きい要素を右。
- [1, 2] | [3] | [5, 4]
- ピボット3は正しい位置に固定される。
- 右側部分配列: [8, 7] (ピボット: 7)
- パーティショニング: 7より小さい要素を左、大きい要素を右。
- [] | [7] | [8]
- ピボット7は正しい位置に固定される。
- さらに再帰的に分割・パーティショニング…
- [1, 2] -> [1] | [2] | [] -> [1, 2]
- [5, 4] -> [4] | [5] | [] -> [4, 5]
- [8] -> [8] (要素1つなので確定)
- 全てが確定したら結合 (物理的な結合ではなく、分割された部分配列がソート済みになることで全体がソート済みになる)
- [1, 2] [3] [4, 5] [6] [7] [8]
- 結果的に: [1, 2, 3, 4, 5, 6, 7, 8]
最終的なソート済み配列: [1, 2, 3, 4, 5, 6, 7, 8]
- 初期配列: [5, 1, 4, 2, 8, 3, 7, 6] (ピボット: 6)
-
計算量:
- 時間計算量:
- 最悪ケース: O(N^2) – ピボットの選択が悪く(常に最小または最大の要素が選ばれるなど)、分割が一様に進まない場合(例: 既にソートされている配列や逆順の配列を、常に端の要素をピボットとしてソートする場合)。この場合、再帰の深さが N に近くなり、各段階でのパーティショニングに O(N) かかるため、合計で O(N^2) となる。
- 平均ケース: O(N log N) – ピボットが適切に選ばれ、配列が一様に分割される場合。再帰の深さが log N となり、各段階で合計 O(N) のパーティショニングが発生するため。
- 最良ケース: O(N log N)
- 空間計算量: O(log N) (平均) / O(N) (最悪) – 再帰呼び出しのスタック領域として必要になります。平均的には分割が一様に進むためスタックの深さは O(log N) ですが、最悪ケース(O(N^2) になるような入力)ではスタックの深さが O(N) になる可能性があります。補助配列を必要としないため、一般的に in-place ソートに分類されます。
- 時間計算量:
-
安定性: 不安定 – パーティショニングの過程で、ピボットと同じ値を持つ要素が元の順序と異なる位置に配置される可能性があります。例えば、[5a, 3, 5b, 1] をピボット5aでソートすると、結果が [3, 1, 5a, 5b] や [3, 1, 5b, 5a] になる可能性があり、5aと5bの元の順序が維持されない場合があります。
-
特徴:
- 平均的な時間計算量が O(N log N) であり、多くの実用的な場面で非常に高速。特に要素の比較と交換が高速なCPUキャッシュ内で効率よく行われるため、理論的な O(N log N) よりも実測でマージソートより速いことが多い。
- O(log N) のスタック領域を除けば in-place ソートである。
- 最悪ケースの性能が O(N^2) である点が欠点。ピボットの選択方法を工夫することで最悪ケースの発生確率を低減できる(例: 中央値の利用、ランダムなピボット選択)。
- 安定ではない。
6. ヒープソート (Heap Sort)
-
概要:
ヒープソートは、データ構造の一つである「ヒープ」(ここでは最大ヒープを仮定)を利用したソートアルゴリズムです。ヒープは、親ノードの値が常に子ノードの値以上(最大ヒープの場合)という特性を持つ木構造を、配列で効率的に表現したものです。ヒープソートは、まず入力配列を最大ヒープの構造に変換し、その後、ヒープの根(最大値)を配列の末尾と交換し、ヒープのサイズを一つ減らして、残りの要素で再びヒープ構造を維持するという操作を繰り返します。 -
手順:
- ヒープ構築 (Build Max Heap): 入力配列を最大ヒープ構造に変換します。配列を木構造とみなし(通常、要素 i の子は 2i+1 と 2i+2、親は (i-1)/2)、配列の末尾から先頭に向かって、各非葉ノードを根とする部分木がヒープの性質を満たすように調整(ヒープ化、Heapify)を行います。
- ソート (Sorting):
- ヒープの根(配列の先頭要素、最大値)を、配列の現在のヒープ部分の末尾の要素と交換します。
- ヒープのサイズを1減らします(交換された最大値はソート済み部分として固定されます)。
- 交換によってヒープの根がヒープの性質を満たさなくなった可能性があるので、根から始めて再びヒープ化を行います。
- 手順2を、ヒープのサイズが1になるまで繰り返します。配列の後ろから順にソート済み要素が配置されていきます。
-
例: 配列 [5, 1, 4, 2, 8] を昇順にソートする
- 初期配列: [5, 1, 4, 2, 8]
- ヒープ構築 (最大ヒープ): 配列を最大ヒープ構造に変換。
- [5, 1, 4, 2, 8] -> ヒープ化 -> [8, 5, 4, 2, 1] (例: 8が根、子は5, 4など)
- ソート:
- 根(8)と末尾(1)を交換: [1, 5, 4, 2, 8]。ソート済み: [8]
- ヒープサイズを減らす (対象: [1, 5, 4, 2])。根(1)をヒープ化: [5, 2, 4, 1]。
- 現在のヒープ: [5, 2, 4, 1]
- 根(5)と末尾(1)を交換: [1, 2, 4, 5, 8]。ソート済み: [8, 5]
- ヒープサイズを減らす (対象: [1, 2, 4])。根(1)をヒープ化: [4, 2, 1]。
- 現在のヒープ: [4, 2, 1]
- 根(4)と末尾(1)を交換: [1, 2, 4, 5, 8]。ソート済み: [8, 5, 4]
- ヒープサイズを減らす (対象: [1, 2])。根(1)をヒープ化: [2, 1]。
- 現在のヒープ: [2, 1]
- 根(2)と末尾(1)を交換: [1, 2, 4, 5, 8]。ソート済み: [8, 5, 4, 2]
- ヒープサイズを減らす (対象: [1])。要素1つなのでソート済み。
最終的なソート済み配列: [1, 2, 4, 5, 8] (逆順に確定していくので、最後に全体を反転するか、最小ヒープを使えば昇順に確定します)
-
計算量:
- 時間計算量:
- 最悪ケース: O(N log N) – ヒープ構築に O(N) かかり、その後の N-1回のヒープ化操作はそれぞれ O(log N) かかるため。
- 平均ケース: O(N log N)
- 最良ケース: O(N log N) – 入力の初期状態に大きく依存しない。
- 空間計算量: O(1) – 配列内でヒープ構造を表現し、要素の交換のみを行うため、補助メモリはほとんど不要(in-place)。
- 時間計算量:
-
安定性: 不安定 – ヒープ化や要素の交換の際に、同じ値を持つ要素の相対的な順序が崩れる可能性があります。
-
特徴:
- 時間計算量が常に O(N log N) であり、クイックソートの最悪ケース O(N^2) を回避できる点で優れている。
- O(1) の空間計算量を持つ in-place ソートである。メモリ効率が良い。
- クイックソートやマージソートに比べて、一般的に同じ O(N log N) でも定数倍の処理時間がかかる傾向がある。これは、ヒープ構造上の要素がメモリ上で離れた位置にあることが多く、CPUキャッシュミスが発生しやすいためと考えられている。
- データが全て揃っていなくても、要素を追加しながらソート済みの最大/最小要素を取り出すといった優先度付きキューの実装にも利用できる。
7. シェルソート (Shell Sort)
-
概要:
シェルソートは、挿入ソートを改良したアルゴリズムです。挿入ソートはほぼソート済みの配列に対して効率が良いという特性に着目し、まず離れた位置にある要素同士を比較・交換することで、配列を「おおよそ」ソートされた状態にします。その後、比較する要素間の距離(ギャップ)を徐々に小さくしていき、最終的にはギャップ1の挿入ソート(通常の挿入ソート)を行います。これにより、挿入ソートの苦手な「小さな要素が配列の末尾にあり、大きくずらさなければならない」といった状況を改善しています。 -
手順:
- 初期のギャップ h を決定します(h > 1)。hの値は配列サイズ N に応じていくつかの方法で決定されます(例: N/2, 3h+1 の数列の逆順など)。
- ギャップ h を持つ要素のグループに対して挿入ソートを適用します。これは、元の配列の i 番目の要素と i+h, i+2h, … 番目の要素からなる部分列に対して挿入ソートを行うことと等価です。
- ギャップ h をより小さな値に変更します(例: h/2)。
- ギャップ h が1になるまで手順2と3を繰り返します。最後のギャップ1での挿入ソートにより、配列全体が完全にソートされます。
-
例: 配列 [5, 1, 4, 2, 8, 3] を昇順にソートする (ギャップ h=3 -> h=1 の順とする)
- 初期配列: [5, 1, 4, 2, 8, 3]
- ギャップ h=3:
- グループ1 (インデックス 0, 3): [5, 2] -> 挿入ソート -> [2, 5]。配列を更新: [2, 1, 4, 5, 8, 3]
- グループ2 (インデックス 1, 4): [1, 8] -> 挿入ソート -> [1, 8]。配列を更新: [2, 1, 4, 5, 8, 3] (変化なし)
- グループ3 (インデックス 2, 5): [4, 3] -> 挿入ソート -> [3, 4]。配列を更新: [2, 1, 3, 5, 8, 4]
- h=3でのソート後: [2, 1, 3, 5, 8, 4] (おおよそソートされた状態)
- ギャップ h=1:
- 配列 [2, 1, 3, 5, 8, 4] に対して通常の挿入ソートを行う。
- 1を挿入: [1, 2, 3, 5, 8, 4]
- 3は正しい位置
- 5は正しい位置
- 8は正しい位置
- 4を挿入: [1, 2, 3, 4, 5, 8]
- 最終的なソート済み配列: [1, 2, 3, 4, 5, 8]
-
計算量:
- 時間計算量: シェルソートの時間計算量は、使用するギャップ列によって大きく変動します。明確な最悪ケースは O(N^2) となりますが、適切なギャップ列を選択することで、平均ケースで O(N log^2 N) または O(N^(3/2))、あるいはさらに良い O(N^(4/3)) や O(N^(7/6)) など、O(N log N) に近い性能を出すことが知られています。最適なギャップ列を見つけることは研究課題であり、特定のギャップ列に対する厳密な解析は難しい場合があります。
- 最悪ケース: O(N^2) (ギャップ選択が悪い場合)
- 平均ケース: O(N log^2 N) や O(N^(4/3)) など、ギャップ列による
- 最良ケース: O(N log N) (完全にソートされている場合、ギャップ1の挿入ソートで O(N))
- 空間計算量: O(1) – 挿入ソートと同様、in-place ソートです。
- 時間計算量: シェルソートの時間計算量は、使用するギャップ列によって大きく変動します。明確な最悪ケースは O(N^2) となりますが、適切なギャップ列を選択することで、平均ケースで O(N log^2 N) または O(N^(3/2))、あるいはさらに良い O(N^(4/3)) や O(N^(7/6)) など、O(N log N) に近い性能を出すことが知られています。最適なギャップ列を見つけることは研究課題であり、特定のギャップ列に対する厳密な解析は難しい場合があります。
-
安定性: 不安定 – 離れた要素同士の交換を行うため、同じ値を持つ要素の相対的な順序が崩れる可能性があります。
-
特徴:
- 実装は挿入ソートより少し複雑になるが、O(N^2) の単純なソートよりも高速。
- O(N log N) の高速ソート(クイックソート、マージソート、ヒープソート)よりは遅いが、O(1) の空間計算量を持つ。
- 配列サイズがあまり大きくない場合(数千〜数万要素程度)には、シンプルさと O(1) 空間計算量から実用的な選択肢となることがある。
- ギャップ列の選択によって性能が大きく変わる。
8. 計数ソート (Counting Sort)
-
概要:
計数ソートは、要素の値が取りうる範囲が比較的狭い整数である場合に非常に効率的なソートアルゴリズムです。要素の「値そのもの」をインデックスとして利用します。これは比較に基づかないソートアルゴリズムであり、時間計算量が O(N) を達成できる可能性があります。 -
手順:
- 入力配列の要素の最大値(または値の範囲)を求めます。この範囲に基づいて、要素の出現回数を記録するための「計数配列 (Count Array)」を作成します。
- 入力配列を走査し、各要素の値に対応する計数配列のインデックスの値をインクリメントします。計数配列には、各値が入力配列に何回出現したかが記録されます。
- 計数配列を累積和に変換します。計数配列の各要素を、その要素までの要素の合計値に置き換えます。これにより、計数配列の各インデックスには、「その値以下の要素が入力配列にいくつ存在するか」が記録されます。これは、ソート後の配列における各要素の「終了位置」を示します。
- 入力配列を後ろから走査します(安定性を保つため)。各要素の値 v に対して、累積和計数配列で示される位置 (count[v]-1) にその要素を配置します。配置後、count[v] の値をデクリメントします。これにより、同じ値の次の要素が、前の要素の隣に配置されることになります。
- 元の入力配列とは別の「出力配列 (Output Array)」にソート結果を格納します。
-
例: 配列 [1, 4, 1, 2, 7, 5, 2] (値の範囲: 0-9) を昇順にソートする
- 初期配列: [1, 4, 1, 2, 7, 5, 2]
- 最大値は7。値の範囲を0-9とする。計数配列 (サイズ10) を作成し、0で初期化: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
- 入力配列を走査し、出現回数をカウント:
- 1: 2回
- 2: 2回
- 4: 1回
- 5: 1回
- 7: 1回
- 計数配列: [0, 2, 2, 0, 1, 1, 0, 1, 0, 0] (インデックス0から9に対応)
- 計数配列を累積和に変換:
- [0, 2, 2, 0, 1, 1, 0, 1, 0, 0]
- 累積和: [0, 0+2, 2+2, 4+0, 4+1, 5+1, 6+0, 6+1, 7+0, 7+0]
- 累積和計数配列: [0, 2, 4, 4, 5, 6, 6, 7, 7, 7] (インデックス0から9に対応)
- 意味: 値1以下の要素が2個、値2以下の要素が4個、… 値7以下の要素が7個存在する。
- 入力配列を後ろから走査し、出力配列に配置 (出力配列はサイズ7で作成):
- 要素 2 (最後): 累積和[2] = 4。出力配列のインデックス 4-1=3 に配置。出力:[, , , 2, , , ]。累積和[2]を3にデクリメント。累積和: [0, 2, 3, 4, 5, 6, 6, 7, 7, 7]
- 要素 5: 累積和[5] = 6。出力配列のインデックス 6-1=5 に配置。出力:[, , , 2, , 5, _]。累積和[5]を5にデクリメント。累積和: [0, 2, 3, 4, 5, 5, 6, 7, 7, 7]
- 要素 7: 累積和[7] = 7。出力配列のインデックス 7-1=6 に配置。出力:[, , , 2, , 5, 7]。累積和[7]を6にデクリメント。累積和: [0, 2, 3, 4, 5, 5, 6, 6, 7, 7]
- 要素 2: 累積和[2] = 3。出力配列のインデックス 3-1=2 に配置。出力:[, , 2, 2, _, 5, 7]。累積和[2]を2にデクリメント。累積和: [0, 2, 2, 4, 5, 5, 6, 6, 7, 7]
- 要素 4: 累積和[4] = 5。出力配列のインデックス 5-1=4 に配置。出力:[, , 2, 2, 4, 5, 7]。累積和[4]を4にデクリメント。累積和: [0, 2, 2, 4, 4, 5, 6, 6, 7, 7]
- 要素 1: 累積和[1] = 2。出力配列のインデックス 2-1=1 に配置。出力:[_, 1, 2, 2, 4, 5, 7]。累積和[1]を1にデクリメント。累積和: [0, 1, 2, 4, 4, 5, 6, 6, 7, 7]
- 要素 1 (先頭): 累積和[1] = 1。出力配列のインデックス 1-1=0 に配置。出力:[1, 1, 2, 2, 4, 5, 7]。累積和[1]を0にデクリメント。累積和: [0, 0, 2, 4, 4, 5, 6, 6, 7, 7]
最終的なソート済み配列 (出力配列): [1, 1, 2, 2, 4, 5, 7]
-
計算量:
- 時間計算量: O(N + K) – N は入力配列のサイズ、K は要素の値の範囲(最大値 – 最小値 + 1)です。入力配列の走査に O(N)、計数配列の初期化に O(K)、計数配列の累積和化に O(K)、出力配列への配置に O(N) かかるため。K が N に対して小さい場合(例: K=O(N))、時間計算量は O(N) となります。K が非常に大きい場合(例: K=O(N^2))、O(N^2) より悪くなる可能性があります。
- 空間計算量: O(N + K) – 計数配列と出力配列のために、合計 O(N + K) の補助メモリが必要です。K が非常に大きい場合、メモリ消費が問題となる可能性があります。
-
安定性: 安定 – 入力配列を後ろから走査し、累積和計数配列を利用して適切な位置に配置することで、同じ値を持つ要素の元の相対的な順序を維持できます。
-
特徴:
- 比較に基づかないソートであり、条件が満たされれば O(N) という高速な時間計算量を達成できる。これは比較ソートの理論限界 O(N log N) を超える速さです。
- 安定なソートアルゴリズムである。
- 要素の値が整数であり、その取りうる範囲 K が N に対して小さい場合にのみ効率的。値の範囲が大きい場合や、浮動小数点数、文字列などのソートには直接適用できない。
- O(K) の補助メモリが必要であり、値の範囲が大きいとメモリを大量に消費する。
9. 基数ソート (Radix Sort)
-
概要:
基数ソートも、比較に基づかないソートアルゴリズムで、O(N) の時間計算量を達成できる可能性があります。要素を数値の各桁(あるいは文字列の各文字)ごとに、最下位桁(または最下位文字)から順に安定なソートアルゴリズム(通常は計数ソートやバケットソート)を用いてソートしていく手法です。 -
手順:
- ソート対象となる要素が持つ「桁数」(または文字列の長さ、数値の最大値の桁数)の中で最大のものを見つけます。
- 最下位桁から開始し、各桁について以下の処理を行います。
- その桁の値に基づいて、入力配列を安定なソートアルゴリズム(例えば計数ソート)でソートします。
- 全ての桁(最下位桁から最上位桁まで)について手順2を繰り返したら、ソートが完了します。
-
例: 配列 [170, 45, 75, 90, 802, 24, 2, 66] を昇順にソートする
- 最大桁数は3 (802)。3桁の数値として扱います(足りない桁は0で埋めると考える)。
- 最下位桁 (1の位) で安定ソート:
- 元の配列: [170, 45, 75, 90, 802, 24, 2, 66]
- 1の位の値: [0, 5, 5, 0, 2, 4, 2, 6]
- 1の位を基準に安定ソート: [170, 90, 802, 2, 24, 45, 75, 66]
- 配列: [170, 90, 802, 2, 24, 45, 75, 66]
- 次の桁 (10の位) で安定ソート:
- 元の配列: [170, 90, 802, 2, 24, 45, 75, 66] (上記のソート済み配列)
- 10の位の値: [7, 9, 0, 0, 2, 4, 7, 6]
- 10の位を基準に安定ソート: [802, 02, 24, 45, 66, 170, 75, 90]
- 配列: [802, 2, 24, 45, 66, 170, 75, 90]
- 最上位桁 (100の位) で安定ソート:
- 元の配列: [802, 2, 24, 45, 66, 170, 75, 90] (上記のソート済み配列)
- 100の位の値: [8, 0, 0, 0, 0, 1, 0, 0] (100未満の数値は0とする)
- 100の位を基準に安定ソート: [002, 024, 045, 066, 075, 090, 170, 802]
- 配列: [2, 24, 45, 66, 75, 90, 170, 802]
最終的なソート済み配列: [2, 24, 45, 66, 75, 90, 170, 802]
-
計算量:
- 時間計算量: O(d * (N + K)) – N は入力配列のサイズ、d は数値の最大桁数(あるいは文字の最大数)、K は各桁(文字)が取りうる値の範囲(例: 10進数なら10、ASCII文字なら256)です。各桁のソートに O(N + K) かかり、これを d 回繰り返すため。数値の場合、最大値 M に対して d は log_K(M) に比例します。したがって、時間計算量は O(N log_K(M)) と表現されることもあります。N が M や d に対して十分に大きい場合、O(N) に近い性能となります。
- 空間計算量: O(N + K) – 各桁のソートに使用する計数ソートやバケットソートが必要とする空間量。
-
安定性: 安定 – 各桁のソートに安定なアルゴリズム(計数ソートなど)を使用することが必須条件です。安定なソートを用いることで、同じ桁の値を持つ要素間の相対的な順序が維持され、最終的に全体のソートが正しく行われます。
-
特徴:
- 比較に基づかないソートであり、特定の条件(数値や文字列で、桁数/文字数が一定範囲内など)を満たせば O(N) に近い時間計算量を達成できる。
- 安定なソートアルゴリズムである。
- 計数ソートと同様、要素が数値や特定の基数で表現できるデータである必要がある。浮動小数点数や一般的なオブジェクトのソートには直接適用できない。
- 各桁のソートを d 回行うため、桁数が多い場合はコストが増加する。
10. バケットソート (Bucket Sort)
-
概要:
バケットソートは、入力データの分布がある程度均一であることが期待できる場合に効率的なソートアルゴリズムです。入力配列の要素を、いくつかの「バケット(桶)」に分割します。各バケットは特定の範囲の値を担当します。要素を対応するバケットに投入した後、各バケット内の要素を個別にソートし、最後に全てのバケットを連結することでソート済み配列を得ます。 -
手順:
- 入力配列の要素の範囲(最小値から最大値)を求め、その範囲をいくつかの区間に分割し、対応するバケットを作成します。バケットの数は N に比例させることが一般的です。
- 入力配列を走査し、各要素をその値が属する区間に対応するバケットに投入します。各バケットはリストや連結リストなどのデータ構造で実装されます。
- 各バケット内の要素を、別途ソートアルゴリズム(例えば挿入ソート、またはバケットサイズが大きい場合は再帰的にバケットソートやクイックソートなど)を用いてソートします。
- バケットを順に連結し、最終的なソート済み配列を作成します。
-
例: 配列 [0.897, 0.565, 0.656, 0.1234, 0.665, 0.3434] (0-1の範囲の浮動小数点数) を昇順にソートする (バケット数10)
- 初期配列: [0.897, 0.565, 0.656, 0.1234, 0.665, 0.3434]
- バケット数10 (インデックス0-9) を用意。
- 各要素を対応するバケットに投入 (例: 0.xy… はバケットxへ):
- 0.897 -> バケット8
- 0.565 -> バケット5
- 0.656 -> バケット6
- 0.1234 -> バケット1
- 0.665 -> バケット6
- 0.3434 -> バケット3
- バケット:
- バケット0: []
- バケット1: [0.1234]
- バケット2: []
- バケット3: [0.3434]
- バケット4: []
- バケット5: [0.565]
- バケット6: [0.656, 0.665]
- バケット7: []
- バケット8: [0.897]
- バケット9: []
- 各バケット内をソート (挿入ソートなど):
- バケット1: [0.1234] -> [0.1234]
- バケット3: [0.3434] -> [0.3434]
- バケット5: [0.565] -> [0.565]
- バケット6: [0.656, 0.665] -> [0.656, 0.665]
- バケット8: [0.897] -> [0.897]
- バケットを順に連結:
- [0.1234] + [0.3434] + [0.565] + [0.656, 0.665] + [0.897]
- 最終的なソート済み配列: [0.1234, 0.3434, 0.565, 0.656, 0.665, 0.897]
-
計算量:
- 時間計算量: O(N + K) または O(N) – N は入力配列のサイズ、K はバケットの数です。要素をバケットに投入するのに O(N) かかります。各バケット内のソートにかかる時間は、バケット内の要素数の二乗に依存しますが、入力データの分布が均一で、要素がバケット間で分散される場合、各バケットのサイズは小さくなり、各バケット内のソートにかかる時間(例えば挿入ソート O(m^2))の合計は O(N) に近くなります。バケット数を N とした場合、平均ケースでは O(N) となります。最悪ケースは、全ての要素が同じバケットに入ってしまい、そのバケットをソートするコストが支配的になる場合で、バケット内のソートアルゴリズムに依存しますが、挿入ソートなら O(N^2) となります。
- 最悪ケース: O(N^2) (全ての要素が1つのバケットに集中)
- 平均ケース: O(N) (データ分布が均一な場合)
- 最良ケース: O(N)
- 空間計算量: O(N + K) – N は入力配列のサイズ、K はバケットの数です。バケットを格納するためのリスト構造や、バケット内のソートに必要な補助空間が必要です。
- 時間計算量: O(N + K) または O(N) – N は入力配列のサイズ、K はバケットの数です。要素をバケットに投入するのに O(N) かかります。各バケット内のソートにかかる時間は、バケット内の要素数の二乗に依存しますが、入力データの分布が均一で、要素がバケット間で分散される場合、各バケットのサイズは小さくなり、各バケット内のソートにかかる時間(例えば挿入ソート O(m^2))の合計は O(N) に近くなります。バケット数を N とした場合、平均ケースでは O(N) となります。最悪ケースは、全ての要素が同じバケットに入ってしまい、そのバケットをソートするコストが支配的になる場合で、バケット内のソートアルゴリズムに依存しますが、挿入ソートなら O(N^2) となります。
-
安定性: 安定 – 各バケット内のソートに安定なアルゴリズムを使用し、かつバケットを順番に連結することで、全体の安定性を維持できます。
-
特徴:
- データ分布が均一であれば、平均 O(N) の高速なソートが可能。
- 比較に基づかないソートであり、比較ソートの理論限界を超えうる。
- 安定なソートアルゴリズムである。
- 入力データの範囲や分布に関する知識が必要。データの分布が偏っている場合は効率が著しく低下する。
- 計数ソートや基数ソートと異なり、浮動小数点数などのソートにも応用しやすい。
11. コムソート (Comb Sort)
-
概要:
コムソートは、バブルソートの改良版です。バブルソートの「隣接する要素のみを比較・交換する」という制限を取り払い、初期のギャップを配列サイズよりも大きく設定し、バブルソートのようにギャップh離れた要素同士を比較・交換します。このギャップを「縮小率」に基づいて徐々に小さくしていき、最終的にギャップ1のバブルソート(通常のバブルソート)を行います。これにより、バブルソートの苦手な「亀(Turtle)」と呼ばれる小さな値が配列の後方に多く存在する場合の非効率性を改善します。推奨される縮小率は約1.3です。 -
手順:
- 初期のギャップ h を配列サイズ N とします。縮小率 r を設定します(通常 1.3)。
- ギャップ h が1より大きくなるか、直前のパスで交換が発生しなくなるまで繰り返します。
- 現在のギャップ h を計算します: h = floor(h / r)。h が1より小さくなったら1とします。
- 配列を走査し、インデックス i と i+h の要素を比較・交換します。これはギャップ h を持つバブルソートです。このパスで交換が発生したかを記録します。
- ギャップが1になった後の最後のパスは、通常のバブルソートと同じになります。
-
例: 配列 [8, 4, 1, 5, 9, 3, 2, 6] を昇順にソートする (縮小率 1.3)
- 初期配列: [8, 4, 1, 5, 9, 3, 2, 6], サイズ N=8, 初期ギャップ h=8
- h = floor(8 / 1.3) = 6
- ギャップ6で比較・交換: (8, 2), (4, 6)
- [2, 4, 1, 5, 9, 3, 8, 6] -> 8と2を交換 -> [2, 4, 1, 5, 9, 3, 8, 6]
- [2, 4, 1, 5, 9, 3, 8, 6] -> 4と6を交換 -> [2, 6, 1, 5, 9, 3, 8, 4]
- 交換発生。配列: [2, 6, 1, 5, 9, 3, 8, 4]
- h = floor(6 / 1.3) = 4
- ギャップ4で比較・交換: (2, 9), (6, 3), (1, 8), (5, 4)
- [2, 6, 1, 5, 9, 3, 8, 4] -> (2,9) 交換なし
- [2, 6, 1, 5, 9, 3, 8, 4] -> (6,3) 交換 -> [2, 3, 1, 5, 9, 6, 8, 4]
- [2, 3, 1, 5, 9, 6, 8, 4] -> (1,8) 交換なし
- [2, 3, 1, 5, 9, 6, 8, 4] -> (5,4) 交換 -> [2, 3, 1, 4, 9, 6, 8, 5]
- 交換発生。配列: [2, 3, 1, 4, 9, 6, 8, 5]
- h = floor(4 / 1.3) = 3
- ギャップ3で比較・交換: (2,5), (3,9), (1,6), (4,8), (9,5) … 繰り返し
- 配列: [1, 2, 3, 4, 5, 6, 8, 9] (途中省略)
- 交換発生。
- h = floor(3 / 1.3) = 2
- ギャップ2で比較・交換 …
- 配列: [1, 2, 3, 4, 5, 6, 8, 9] (途中省略)
- 交換発生。
- h = floor(2 / 1.3) = 1
- ギャップ1で比較・交換 (通常のバブルソート) …
- 配列: [1, 2, 3, 4, 5, 6, 8, 9]
- 交換なし。ループ終了。
最終的なソート済み配列: [1, 2, 3, 4, 5, 6, 8, 9]
-
計算量:
- 時間計算量: ギャップ列と縮小率に依存します。適切な縮小率 (約1.3) を使用した場合、平均ケースは O(N log N) に近い性能を発揮することが経験的に知られています。理論的な最悪ケースはまだ完全に解析されていませんが、O(N log^2 N) と考えられることが多いです。
- 最悪ケース: O(N^2)
- 平均ケース: O(N log N) ~ O(N log^2 N)
- 最良ケース: O(N log N) (完全にソートされている場合、最後のギャップ1パスは O(N))
- 空間計算量: O(1) – バブルソートと同様、in-place ソートです。
- 時間計算量: ギャップ列と縮小率に依存します。適切な縮小率 (約1.3) を使用した場合、平均ケースは O(N log N) に近い性能を発揮することが経験的に知られています。理論的な最悪ケースはまだ完全に解析されていませんが、O(N log^2 N) と考えられることが多いです。
-
安定性: 不安定 – ギャップが1より大きい場合の比較・交換で、離れた要素同士の相対的な順序が崩れる可能性があります。
-
特徴:
- バブルソートより大幅に高速で、O(N^2) ソート群の中では比較的優れた性能を持つ。
- O(1) の空間計算量を持つ in-place ソートである。
- 実装は単純な O(N^2) ソートと O(N log N) ソートの中間程度の複雑さ。
- ギャップの縮小率が性能に影響する。1.3が推奨されている。
12. イントロソート (Intro Sort)
-
概要:
イントロソートは、実際の標準ライブラリ(C++のstd::sort
など)でよく採用されているハイブリッドソートアルゴリズムです。クイックソートの平均ケースでの高速性と、最悪ケース O(N^2) の問題を克服することを目的としています。イントロソートは、再帰の深さを制限したクイックソートとして開始し、もし再帰の深さが一定のしきい値(通常は log N に比例)を超えた場合、または部分配列のサイズが一定のしきい値以下になった場合に、別のソートアルゴリズムに切り替えます。 -
手順:
- クイックソートを開始します。ピボットを選択し、パーティショニングを行います。
- 再帰的に左右の部分配列に対してソートを適用します。
- 再帰の深さが事前に設定した最大深さ(例: 2 * log N)を超えた場合、不安定なクイックソートの最悪ケースに陥る可能性が高いと判断し、ヒープソートに切り替えます。ヒープソートは常に O(N log N) の性能を保証するため、全体の最悪ケースを O(N log N) に抑えることができます。
- 分割された部分配列のサイズが小さくなった場合(例: 16要素以下など)、要素数が少ない配列に対して高速な挿入ソートに切り替えます。挿入ソートは小さな配列に対してクイックソートよりも効率が良いことが多いためです。
- 部分配列のサイズが1以下になったら再帰を終了します。
-
計算量:
- 時間計算量:
- 最悪ケース: O(N log N) – 再帰が深くなりすぎた場合にヒープソートに切り替えるため、最悪ケースでも O(N log N) が保証されます。
- 平均ケース: O(N log N) – 基本的にはクイックソートの平均性能が支配的です。
- 最良ケース: O(N log N)
- 空間計算量: O(log N) (平均) – クイックソートの再帰スタックに必要な領域(通常 in-place と見なされる)。ヒープソートは O(1) 空間、挿入ソートも O(1) 空間なので、全体の空間計算量はクイックソートのスタック深さに依存します。
- 時間計算量:
-
安定性: 不安定 – ベースとなっているクイックソートやヒープソートが不安定なため、イントロソートも不安定です。
-
特徴:
- 平均ケースでのクイックソートの高速性を活かしつつ、最悪ケース O(N^2) をヒープソートへの切り替えによって回避する。これにより、非常に高い信頼性と効率を両立している。
- 小さな配列には挿入ソートを利用することで、オーバーヘッドを削減し、さらに高速化を図っている。
- 多くのプログラミング言語の標準ライブラリで採用されており、実用上最もよく使われるソートアルゴリズムの一つ。
- in-place ソートである。
- 不安定なソートである。
ソートアルゴリズム比較表
アルゴリズム | 時間計算量 (最悪) | 時間計算量 (平均) | 空間計算量 (補助) | 安定性 | In-place | 実装の容易さ | 特徴 |
---|---|---|---|---|---|---|---|
バブルソート | O(N^2) | O(N^2) | O(1) | 安定 | Yes | とても容易い | 最も単純。教育向け。ほぼソート済みならO(N)。 |
選択ソート | O(N^2) | O(N^2) | O(1) | 不安定 | Yes | 容易い | 交換回数が少ない。入力によらず一定性能 (遅い)。 |
挿入ソート | O(N^2) | O(N^2) | O(1) | 安定 | Yes | 容易い | 小規模データやほぼソート済みデータに高速。オンラインソート可能。 |
マージソート | O(N log N) | O(N log N) | O(N) | 安定 | No | 中程度 | 常に安定 O(N log N)。並列化容易。O(N)空間が必要。 |
クイックソート | O(N^2) | O(N log N) | O(log N) (Avg) | 不安定 | Yes | 中程度 | 平均的に最速。最悪ケースO(N^2)に注意。 |
ヒープソート | O(N log N) | O(N log N) | O(1) | 不安定 | Yes | やや難しい | 常に安定 O(N log N), O(1)空間。実測は他のO(N log N)より遅い傾向。 |
シェルソート | O(N^2) (Worst) | O(N log^2 N) など | O(1) | 不安定 | Yes | 中程度 | 挿入ソート改良版。O(N^2)より高速なO(1)空間ソート。ギャップに依存。 |
計数ソート | O(N+K) | O(N+K) | O(N+K) | 安定 | No | 容易い | 値の範囲Kが狭い整数にO(N)。非比較ソート。 |
基数ソート | O(d(N+K)) | O(d(N+K)) | O(N+K) | 安定 | No | やや難しい | 数値/文字列にO(N)。非比較ソート。桁数dと基数Kに依存。 |
バケットソート | O(N^2) (Worst) | O(N) (Avg) | O(N+K) | 安定 | No | 中程度 | 分布が均一なデータにO(N)。非比較ソート。 |
イントロソート | O(N log N) | O(N log N) | O(log N) (Avg) | 不安定 | Yes | 難しい | クイックソート+ヒープソート+挿入ソートのハイブリッド。実用上最速。 |
注: K は値の範囲、d は桁数を表します。O(log N) は再帰スタックの深さを示します。In-place は補助メモリが O(1) であることを指すのが一般的です。
アルゴリズム選択のポイント
これまでに見てきたように、各ソートアルゴリズムには一長一短があります。どのようなアルゴリズムを選択すべきかは、状況に応じて総合的に判断する必要があります。考慮すべき主なポイントを以下に示します。
-
データの規模:
- データ数が少ない場合(数百〜数千程度):単純な O(N^2) ソート(挿入ソートなど)でも十分な速度が出ることが多く、実装が容易であるという利点があります。特に挿入ソートは小さなデータに強いです。
- データ数が多い場合(数万以上):O(N log N) のソート(マージソート、クイックソート、ヒープソート、イントロソートなど)を選択することが必須です。O(N^2) のソートでは現実的な時間で処理が完了しません。
-
データの特性:
- 値の範囲が狭い整数: 計数ソートや基数ソートが非常に高速な選択肢となります(O(N))。
- データがほぼソート済み: 挿入ソートやバブルソート(改良版含む)が O(N) に近くなり効率的です。
- データ分布が均一な数値/浮動小数点数: バケットソートが O(N) に近い性能を発揮する可能性があります。
- ランダムなデータ: クイックソートが平均的に高速です。ただし最悪ケースに注意が必要な場合はヒープソートやイントロソートが推奨されます。
-
計算時間への要求:
- とにかく最速の平均性能が必要:クイックソート、またはイントロソート。
- 最悪ケースでも高速性能を保証したい:マージソート、ヒープソート、イントロソート。
-
メモリ使用量の制限:
- 補助メモリをほとんど使用できない (O(1) 空間が必要):選択ソート、挿入ソート、ヒープソート、シェルソート、コムソート、または工夫したクイックソート (再帰深度を制限するなど)。
- O(N) 程度の補助メモリも許容できる:マージソート、計数ソート、基数ソート、バケットソート。
-
安定性の必要性:
- 同じ値を持つ要素の相対的な順序を維持する必要がある:バブルソート、挿入ソート、マージソート、計数ソート、基数ソート、バケットソート。
- 安定性が不要:選択ソート、クイックソート、ヒープソート、シェルソート、コムソート、イントロソート。
-
実装の容易さ:
- 簡単な実装で済ませたい:バブルソート、選択ソート、挿入ソート。教育目的や簡単なツールの作成など。
- 標準ライブラリを利用できる環境であれば、多くの言語で提供されている組み込みのソート関数(内部的にはイントロソートなどが使われていることが多い)を使用するのが、性能と信頼性の観点から最も推奨されます。
まとめ
ソートアルゴリズムは、単にデータを順番に並べ替えるだけでなく、その後の様々な処理効率を決定するコンピュータサイエンスの基礎技術です。バブルソート、選択ソート、挿入ソートのような単純な O(N^2) アルゴリズムは、実装が容易で小規模データには使えますが、大規模データには向きません。一方、マージソート、クイックソート、ヒープソートのような O(N log N) アルゴリズムは、大規模データに対して効率的であり、実用的な場面で広く利用されています。さらに、計数ソート、基数ソート、バケットソートは、データの特性によっては O(N) という優れた性能を発揮します。シェルソートやコムソートは O(N^2) と O(N log N) の中間の性能を持つ O(1) 空間ソートとして、特定の状況で有用です。そして、今日の多くのシステムで利用されている標準ソート関数は、クイックソートの高速性とヒープソートの頑健性、挿入ソートの小配列での効率を組み合わせたハイブリッドアルゴリズムであるイントロソートを基盤としていることが多いです。
アルゴリズムを選択する際には、「時間計算量」と「空間計算量」、そして「安定性」という3つの主要な評価基準を理解し、対象となるデータの性質やシステム要件(メモリ制約、安定性の必要性など)を考慮することが重要です。最適なアルゴリズムは一つではなく、状況によって常に変化します。
この記事を通じて、様々なソートアルゴリズムの仕組みとそれぞれの長所・短所を深く理解していただけたなら幸いです。アルゴリズムの知識は、効率的なプログラム設計と問題解決のための強力な武器となります。ぜひ、ここで得た知識を実際のプログラミングに活かしてください。