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)**是最簡單的排序演算法之一。它透過重複遍歷陣列,比較相鄰元素並在順序錯誤時交換它們。這個演算法的名稱來自於較小的元素會像「泡泡」一樣浮到陣列的頂端(開頭),而較大的元素則「沉降」到底部(末尾)。

演算法會持續遍歷陣列,直到不再需要交換為止,這表示陣列已經排序完成。每次完整遍歷都能保證最大的未排序元素移動到末尾的正確位置。

關鍵重點

  • 基於比較: 泡沫排序比較相鄰的元素對
  • 穩定性: 相等的元素在排序後仍保持其相對順序
  • 原地排序: 僅需要 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 旗標的優化可以顯著提升接近排序完成的陣列的效能
  • 記住每次遍歷至少會將一個元素移動到最終位置

陷阱:

  • 忘記縮小內層迴圈範圍(使用 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) 排序