コーディング · アルゴリズム
バブルソートの仕組み・コード・計算量をわかりやすく解説
結論
バブルソートは、リストを端から端まで走査しながら、隣り合う要素の組が逆順であれば交換することを繰り返す、単純なソートアルゴリズムです。1回のパスが終わるごとに、残りの要素の中で最大のものが泡のように末尾の最終位置まで移動します。平均・最悪ケースで時間は O(n²)、すでにソート済みのリストでは O(n)、追加の空間は O(1) で、安定ソートです。主に教育用で、実際のプログラムでは組み込みのソートを使います。
行の並べ替えを開く考え方を平易に説明
最初の2要素を比較し、前のほうが大きければ交換します。次に2番目と3番目、3番目と4番目と、リストの末尾まで続けます。これが1パスです。1回目のパスの後、すべての比較で大きい値が1つ右に運ばれるため、最大の要素は必ず末尾にあります。
未ソートの部分に対してパスを繰り返します。この部分は毎回1要素ずつ小さくなります。1回のパスで交換がまったく起こらなければ、リストはソート済みなので、そこで早期に終了できます。
具体例をパスごとに見る
[5, 1, 4, 2, 8] を昇順にソートします。バーの後ろの要素は最終位置に収まっています。
| パス | 比較と交換 | パス後のリスト |
|---|---|---|
| 1 | 5↔1 交換、5↔4 交換、5↔2 交換、5 と 8 はそのまま | [1, 4, 2, 5, 8](8 が確定) |
| 2 | 1 と 4 はそのまま、4↔2 交換、4 と 5 はそのまま | [1, 2, 4, 5, 8](5 が確定) |
| 3 | 1 と 2 はそのまま、2 と 4 はそのまま、交換なし | [1, 2, 4, 5, 8](ソート完了、終了) |
早期終了がなければ、アルゴリズムは4回目のパスを実行します。早期終了があれば、この例では3パスで十分です。
Pythonでの実装
def bubble_sort(items):
"""Sort a list in place, ascending. Returns the same list."""
n = len(items)
for end in range(n - 1, 0, -1): # last index of the unsorted part
swapped = False
for i in range(end):
if items[i] > items[i + 1]: # strict >, keeps equal items in order
items[i], items[i + 1] = items[i + 1], items[i]
swapped = True
if not swapped: # no swaps: already sorted
break
return items
print(bubble_sort([5, 1, 4, 2, 8])) # [1, 2, 4, 5, 8]
print(bubble_sort([3, 3, 1])) # [1, 3, 3]
print(bubble_sort([])) # []内側のループは、end より後ろがすでに確定しているため、end で止まります。swapped フラグによって、ソート済みの入力では最良ケースの O(n) になります。
時間計算量と空間計算量
| ケース | 比較回数 | 時間 | 理由 |
|---|---|---|---|
| 最良(すでにソート済み) | n − 1 | O(n) | 交換のない1パスの後で終了する |
| 平均(ランダムな順序) | 約 n²/2 | O(n²) | 交換は平均で約 n²/4 回 |
| 最悪(逆順) | n(n − 1)/2 | すべての比較が交換になる | every comparison is a swap |
| 追加の空間 | O(1) | 少数の変数だけでインプレースにソートする |
逆順の1万要素では、ほぼ5,000万回の比較と交換になります。交換の回数は常に転倒数(順序が逆になっている要素のペアの数)と等しくなります。
バブルソートと他のソートの比較
| アルゴリズム | 最良 | 平均 | 最悪 | 追加の空間 | 安定 |
|---|---|---|---|---|---|
| バブルソート | 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(n) | はい |
| クイックソート | 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(1) | いいえ |
Timsort(Pythonの sorted) | O(n) | O(n log n) | O(n log n) | O(n) | はい |
バブルソートが安定なのは、隣り合う要素だけを、しかも厳密に逆順のときだけ交換するため、等しい要素が互いを追い越さないからです。挿入ソートも同じ計算量と安定性を持ちますが、実際には書き込み回数が少なく高速なので、Timsort のようなハイブリッドアルゴリズムの内部で使われるのは、バブルソートではなく挿入ソートです。
派生と、よくある間違い
- シェーカーソート(カクテルソート)は、パスごとに左から右、右から左と交互に進み、末尾にある小さな要素(「亀」と呼ばれます)を先頭へ速く運びます。それでも O(n²) です。
- コムソートは、一定の間隔だけ離れた要素を比較し、その間隔を縮めていくことで、多くの亀を早い段階で取り除きます。間隔が 1 になるとバブルソートそのものになります。
- 奇偶転置ソートは、奇数番目のペアをすべて比較し、次に偶数番目のペアをすべて比較する方法で、並列ソートの教材として使われます。
バブルソートを使う場面
本番コードでは、ほとんど使いません。Pythonでは sorted(items) か items.sort() を呼びます。これらは Timsort を使い、O(n log n) で動作し、安定です。それでもバブルソートを知っておく価値があるのは、最も説明しやすいソートであり、正しさの証明も追いやすく、ループ・交換・計算量に関する面接や試験の最初の定番問題だからです。
唯一の実用上の利点は、ソート済みのリストを O(n) の1パスで検出できることですが、隣同士を比べる単純なループなら、何もソートせずに同じことができます。
よくある質問
バブルソートは安定ですか?
はい。左の要素が厳密に大きいときにだけ隣り合う要素を交換するので、等しい要素は元の順序を保ちます。
バブルソートの時間計算量は?
平均・最悪ケースで O(n²) です。交換が起こらなかったパスで早期終了すれば、すでにソート済みの入力での最良ケースは O(n) になります。
なぜバブルソートと呼ばれるのですか?
大きな要素が、水面に浮かぶ泡のように、1回の交換ごとにリストの末尾へ向かって移動するからです。k 回目のパスの後には、大きいほうから k 個の要素が確定しています。
バブルソートは挿入ソートより速いですか?
いいえ。どちらも O(n²) ですが、挿入ソートのほうが通常は操作回数が少なく、特にほぼソート済みのデータでは実際に速く動きます。
バブルソートは追加のメモリを使いますか?
いいえ。インプレースでソートし、ループのインデックスと交換時の一時変数のための O(1) の追加空間しか必要としません。