KC Blog

アルゴリズム日記

アルゴリズム日記 - Bubble Sort

著者 WaitZ
Sorting AlgorithmsbeginnerBubble SortO(n^2)O(1)algorithmsbubble-sortbeginner

今日のトピック (Today's Topic)

トピック: Bubble Sort
難易度: beginner
カテゴリー: Sorting Algorithms
時間計算量: O(n^2)
空間計算量: O(1)

概念学習 (Concept Learning)

核心概念

**バブルソート(Bubble Sort)**は、最もシンプルなソートアルゴリズムの1つです。配列を繰り返し走査し、隣接する要素を比較して、順序が間違っている場合は交換します。このアルゴリズムは、小さい要素が配列の先頭に「泡のように浮かび上がり」、大きい要素が末尾に「沈む」ことからこの名前が付けられました。

アルゴリズムは、交換が不要になるまで配列を繰り返し走査します。これは配列がソートされたことを示します。各完全な走査により、最大の未ソート要素が末尾の正しい位置に移動することが保証されます。

重要ポイント

  • 比較ベース: バブルソートは隣接する要素のペアを比較します
  • 安定: 等しい要素はソート後も相対的な順序を維持します
  • インプレース: O(1)の追加メモリスペースのみが必要です
  • シンプルだが非効率: 理解と実装は簡単ですが、大規模なデータセットでは性能が悪いです
  • 適応的: 配列が既にソートされているかを検出するように最適化できます

アルゴリズムステップ

  1. 最初から開始: 配列の最初の要素から始めます
  2. 隣接ペアを比較: 隣接する要素のペア(arr[i]とarr[i+1])を比較します
  3. 必要に応じて交換: 要素が間違った順序の場合(arr[i] > arr[i+1])、交換します
  4. 末尾まで続ける: 次のペアに移動し、配列の末尾に達するまで繰り返します
  5. パスを繰り返す: 各完全なパスの後、最大の要素が正しい位置に配置されます
  6. 範囲を縮小: 後続の各パスで、最後のソート済み要素を無視します
  7. 完了をチェック: パスで交換が発生しなかった場合、配列はソート済みで停止できます

実装 (Implementation)

JavaScript実装

function bubbleSort(arr) {
    const n = arr.length;
    
    // 外側のループ:パスの回数
    for (let i = 0; i < n - 1; i++) {
        // 内側のループ:隣接要素の比較
        for (let j = 0; j < n - i - 1; j++) {
            // 要素が間違った順序の場合は交換
            if (arr[j] > arr[j + 1]) {
                // 分割代入を使用して交換
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
            }
        }
    }
    
    return arr;
}

// 使用例
const array = [64, 34, 25, 12, 22, 11, 90];
console.log("元の配列:", array);
console.log("ソート済み:", bubbleSort([...array]));
// 出力: ソート済み: [11, 12, 22, 25, 34, 64, 90]

最適化版(早期終了)

function bubbleSortOptimized(arr) {
    const n = arr.length;
    
    for (let i = 0; i < n - 1; i++) {
        let swapped = false;
        
        for (let j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
                swapped = true;
            }
        }
        
        // 交換が発生しなかった場合、配列は既にソート済み
        if (!swapped) break;
    }
    
    return arr;
}

Kotlin実装

fun bubbleSort(arr: IntArray): IntArray {
    val n = arr.size
    
    for (i in 0 until n - 1) {
        var swapped = false
        
        for (j in 0 until n - i - 1) {
            if (arr[j] > arr[j + 1]) {
                // 要素を交換
                val temp = arr[j]
                arr[j] = arr[j + 1]
                arr[j + 1] = temp
                swapped = true
            }
        }
        
        // 既にソート済みの場合は早期終了
        if (!swapped) break
    }
    
    return arr
}

// 使用例
fun main() {
    val array = intArrayOf(64, 34, 25, 12, 22, 11, 90)
    println("元の配列: ${array.contentToString()}")
    bubbleSort(array)
    println("ソート済み: ${array.contentToString()}")
}

説明

  • 解法の核心概念: 配列全体がソートされるまで、隣接要素を繰り返し比較して交換します
  • 時間計算量:
    • 最良の場合: O(n) - 配列が既にソート済みの場合(最適化あり)
    • 平均の場合: O(n²) - 配列を複数回走査する必要があります
    • 最悪の場合: O(n²) - 配列が逆順にソートされている場合
  • 空間計算量: O(1) - 一時変数用の定数量の追加スペースのみを使用します
  • エッジケースと一般的な誤り:
    • 空の配列または単一要素: 既にソート済み、そのまま返します
    • すべての要素が同一: 交換は不要、最適化により早期終了します
    • 内側のループ範囲を減らすこと(n - i - 1)を忘れると、既にソート済みの要素の比較が無駄になります
    • 最適化フラグを実装しないと、不要な反復が発生する可能性があります

練習問題 (Practice Problems)

問題リスト

  1. LeetCode 912 - Sort an Array

    • 難易度: Medium
    • バブルソートを適用して整数配列をソートします
    • 注: この問題には最適ではありませんが、練習には良いです
  2. 基本的な実装

    • 配列 [5, 2, 8, 1, 9] をバブルソートでソートします
    • 行われた交換の回数をカウントします
  3. カスタム比較関数を使用したソート

    • バブルソートを使用して、文字列の配列を長さでソートします
    • 例: ["apple", "pie", "banana", "cat"] → ["pie", "cat", "apple", "banana"]
  4. 最適化チャレンジ

    • 最適化版を実装してテストします
    • 既にソートされた配列と逆順にソートされた配列での反復を比較します
  5. バブルソートの可視化

    • 各パス後の配列の状態を出力します
    • 要素がどのように位置に「泡立つ」かを観察します

解法ノート

重要点:

  • インデックスの範囲外エラーを避けるため、常に配列の境界をチェックします
  • swappedフラグを使った最適化は、ほぼソートされた配列のパフォーマンスを大幅に向上させます
  • 各パスで少なくとも1つの要素が最終位置に移動することを覚えておきます

落とし穴:

  • 内側のループの境界を減らすこと(n - i - 1を使用)を忘れる
  • 空の配列や単一要素配列などのエッジケースを処理しない
  • 大規模なデータセットにバブルソートを使用しようとする(代わりにクイックソートやマージソートを使用)

ベストプラクティス:

  • 常に早期終了を伴う最適化版を実装します
  • バブルソートは小規模なデータセット(< 100要素)または教育目的にのみ使用します
  • 安定性の要件を考慮します:バブルソートは等しい要素の相対的な順序を維持します

重要な詳細 (Important Details)

注意すべきエッジケース

  • 空の配列 []: そのまま返します
  • 単一要素 [5]: 既にソート済み
  • 既にソート済み [1, 2, 3, 4]: 最適化版は最初のパス後に終了します
  • 逆順にソート済み [4, 3, 2, 1]: 最悪の場合、最大の交換が必要です
  • すべて重複 [5, 5, 5, 5]: 交換は不要、早期終了します

適用シナリオと制限

使用すべき場合:

  • 教育目的(ソートアルゴリズムの学習)
  • 非常に小規模なデータセット(< 10-20要素)
  • ほぼソート済みの配列(最適化あり)
  • 効率性よりもシンプルさが重要な場合
  • 安定したソートが必要で、データセットが小さい場合

制限:

  • 性能が悪い: O(n²)の時間計算量は大規模なデータセットには遅すぎます
  • 実用的でない: 本番環境では、より良いアルゴリズム(マージソート、クイックソート、ヒープソート)が存在します
  • 多くの比較: 最適化版でも多くの比較が必要です

拡張アプリケーションやバリエーション

  1. 双方向バブルソート(カクテルソート)

    • 前方と後方のパスを交互に行います
    • 場合によっては少し効率的です
  2. フラグ付きバブルソート

    • 交換が発生したかを追跡して早期終了を可能にします
    • 最良の場合の時間計算量をO(n)に改善します
  3. 交換のカウント

    • 最小操作に関する面接の質問に役立ちます
    • 配列の「ソート済み度」を分析するのに役立ちます
  4. 異なるデータ型のソート

    • オブジェクト、文字列、カスタム型用の比較関数を適応させます
    • 比較関数の使用を練習します

今日の振り返り (Daily Reflection)

重要な収穫: バブルソートは比較ベースのソートの基礎を教える基本的なアルゴリズムです。大規模なデータセットには効率的ではありませんが、アルゴリズム分析、ループ不変条件、最適化技術への優れた入門です。バブルソートを理解することは、より洗練されたソートアルゴリズムを評価するのに役立ちます。

関連アルゴリズム:

  • 選択ソート: 類似のO(n²)計算量ですが、異なるアプローチ(各パスで最小値を見つける)を使用します
  • 挿入ソート: O(n²)ですが、小規模/ほぼソート済みの配列では実際にはより効率的です
  • カクテルソート: バブルソートの双方向バリアント
  • マージソート: 効率的なO(n log n)の分割統治ソート
  • クイックソート: 平均的な場合O(n log n)の人気のあるソートアルゴリズム
  • ヒープソート: ヒープデータ構造を使用したO(n log n)のソート