コーディング · アルゴリズム
選択ソートの仕組み・コード・計算量をわかりやすく解説
結論
選択ソートは、リストを左側のソート済み部分と右側の未ソート部分に分けます。各ステップで未ソート部分の最小の要素を見つけ、未ソート部分の先頭位置と交換します。比較回数は常に約 n²/2 回なので、どのケースでも O(n²) ですが、交換は最大でも n − 1 回で、追加の空間は O(1) です。一般的な配列版は安定ではありません。
行の並べ替えを開く考え方を平易に説明
リスト全体を走査して最小の要素を見つけ、インデックス 0 の要素と交換します。これでインデックス 0 は確定です。次にインデックス 1 からリストの残りを走査して最小の要素を見つけ、インデックス 1 と交換します。要素が1つだけ残るまで続けると、その1つは必然的に最大の要素です。
バブルソートや挿入ソートと違い、選択ソートは入力がすでにソート済みでも得をしません。最小値を見つけたと確信するために、残りの要素をすべて走査しなければならないからです。
具体例をステップごとに見る
[64, 25, 12, 22, 11] をソートします。バーより前の部分はソート済みで確定です。
| ステップ | 未ソート部分 | 見つかった最小値 | 交換 | ステップ後のリスト |
|---|---|---|---|---|
| 1 | 64, 25, 12, 22, 11 | インデックス 4 の 11 | インデックス 0 ↔ 4 | [11 | 25, 12, 22, 64] |
| 2 | 25, 12, 22, 64 | インデックス 2 の 12 | インデックス 1 ↔ 2 | [11, 12 | 25, 22, 64] |
| 3 | 25, 22, 64 | インデックス 3 の 22 | インデックス 2 ↔ 3 | [11, 12, 22 | 25, 64] |
| 4 | 25, 64 | インデックス 3 の 25 | 交換不要 | [11, 12, 22, 25 | 64] |
5要素に対して4ステップ、比較は 4 + 3 + 2 + 1 = 10 回、実際の交換は3回です。
Pythonでの実装
def selection_sort(items):
"""Sort a list in place, ascending. Returns the same list."""
n = len(items)
for i in range(n - 1):
smallest = i
for j in range(i + 1, n):
if items[j] < items[smallest]:
smallest = j
if smallest != i:
items[i], items[smallest] = items[smallest], items[i]
return items
print(selection_sort([64, 25, 12, 22, 11])) # [11, 12, 22, 25, 64]
print(selection_sort([2, 1])) # [1, 2]最後に残った要素は自動的に所定の位置にあるため、外側のループは n − 1 回実行されます。内側のループはインデックスを記録するだけで、交換は外側のステップごとに1回だけ行われます。
時間計算量と空間計算量
| 指標 | 値 | 理由 |
|---|---|---|
| 比較回数(すべてのケース) | n(n − 1)/2 | 各ステップで未ソート部分の全体を走査する |
| 時間(最良/平均/最悪) | O(n²) | 走査が順序に依存しない |
| 交換回数(最悪) | n − 1 | ステップごとに最大1回の交換 |
| 追加の空間 | O(1) | インプレース、インデックス変数が1つ |
書き込み回数が少ないことが、選択ソート唯一の本当の強みです。一部のフラッシュメモリや大きなレコードの移動のように、メモリへの書き込みが読み取りよりずっと高コストな場合、書き込みが少ないことが効いてきます。サイクルソートはこれをさらに進め、各要素を最大1回だけ書き込みます。
安定性:選択ソートが安定でない理由
安定ソートは、等しい要素の元の相対的な順序を保ちます。選択ソートの離れた位置との交換は、要素を、それと等しい別の要素の向こう側へ飛び越えさせてしまうことがあります。
def selection_sort_by_key(items, key):
n = len(items)
for i in range(n - 1):
m = min(range(i, n), key=lambda j: key(items[j]))
items[i], items[m] = items[m], items[i]
return items
cards = [("4", "hearts"), ("4", "spades"), ("2", "clubs")]
print(selection_sort_by_key(cards, key=lambda c: c[0]))
# [('2', 'clubs'), ('4', 'spades'), ('4', 'hearts')] order of the 4s changed2 はハートの 4 と交換され、その結果ハートの 4 がスペードの 4 の後ろに移動しました。安定版も存在します。交換する代わりに、最小値を取り除いて位置 i に挿入し、他の要素を右にずらすのです。これで順序は保たれますが、ステップごとに O(n) 回の書き込みが必要になります。
選択ソートと他のソートの比較
| アルゴリズム | 最良 | 平均 | 最悪 | 追加の空間 | 安定 |
|---|---|---|---|---|---|
| 選択ソート | O(n²) | O(n²) | O(n²) | O(1) | いいえ |
| バブルソート | O(n) | O(n²) | O(n²) | O(1) | はい |
| 挿入ソート | O(n) | O(n²) | O(n²) | O(1) | はい |
| ヒープソート | O(n log n) | O(n log n) | O(n log n) | O(1) | いいえ |
| マージソート | O(n log n) | O(n log n) | O(n log n) | O(n) | はい |
| クイックソート | O(n log n) | O(n log n) | O(n²) | O(log n) | いいえ |
ヒープソートは、より良いデータ構造を使った選択ソートです。やはり残りの要素から最大のものを繰り返し選びますが、O(n) の走査ではなく二分ヒープを使って O(log n) で見つけるため、全体で O(n log n) になります。
選択ソートを使う場面
実際のPythonコードでは、組み込みの sorted() か list.sort() を使います。選択ソートを知っておく価値があるのは、教育用の例として、制約のある環境での非常に小さな固定サイズの配列に対して、そして書き込み回数を最小にしなければならない場合です。試験や面接の定番問題でもあり、たいていは、手順を追跡し、比較と交換の回数を数え、安定でない理由を説明する、という形で出題されます。
よくある質問
選択ソートは安定ですか?
標準的なインプレースの配列版は安定ではありません。離れた位置との交換によって、要素が等しい要素を飛び越えてしまうことがあるためです。交換の代わりに挿入を行う版は安定ですが、書き込みが増えます。
選択ソートの時間計算量は?
最良・平均・最悪のどのケースでも O(n²) です。入力の順序に関係なく、常に n(n − 1)/2 回の比較を行います。
選択ソートは何回交換しますか?
最大でも n − 1 回で、外側のステップごとに1回です。O(n²) 回の交換やシフトを行いうるバブルソートや挿入ソートより少なくなります。
選択ソートと挿入ソートの違いは何ですか?
選択ソートは、未ソート部分の最小値を選んでソート済み部分の末尾に追加します。挿入ソートは、次の要素を取り出して、ソート済み部分の正しい位置に挿入します。挿入ソートは安定で、最良ケースが O(n) ですが、選択ソートはどちらも満たしません。