# 演算法導論

::: tip 前言
**如何有效率地解決問題？** 你可能遇過這樣的困惑：同一個問題，有人寫的程式跑幾秒就出結果，有人寫的跑幾分鐘還在轉。差別往往在於演算法。本章帶你理解演算法的核心思維方式。
:::

**這篇文章會帶你學什麼？**

學完這章後，你將獲得：

- **問題拆解能力**：面對複雜問題，能想到用分治、遞迴等策略拆解，而不是一上來就寫程式碼
- **效率判斷能力**：用大 O 表示法判斷兩種解法哪個更高效，而不是憑感覺猜測
- **複雜度思維**：寫程式碼前先估算資料規模和時間要求，選擇合適的演算法級別
- **後續學習基礎**：為進階資料結構、分散式系統、機器學習打下基礎

| 章节 | 內容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 二分搜尋 | 分治思想、O(log n) |
| **第 2 章** | 排序演算法 | 氣泡排序、快速排序、合併排序 |
| **第 3 章** | 複雜度分析 | 時間複雜度、空間複雜度 |

---

## 0. 全景圖：演算法概述

想象你要在一本字典裡找一個單詞：

- **方法一**：從第一頁開始，一頁一頁翻（線性搜尋）
- **方法二**：根據首字母定位，再二分搜尋（二分搜尋）

兩種方法都能找到，但效率天差地別。**演算法就是解決問題的方法**。

<AlgorithmDemo />

**演算法的核心指標：**

| 指標 | 含義 | 為什麼重要 |
|------|------|-----------|
| **時間複雜度** | 執行時間隨資料量增長的趨勢 | 預測大規模資料的效能 |
| **空間複雜度** | 記憶體佔用隨資料量增長的趨勢 | 評估記憶體消耗 |
| **正確性** | 是否總能得到正確結果 | 演算法的基本要求 |

::: tip 逐行解讀這張表
**時間複雜度**：用大 O 表示法描述。O(n) 表示資料量翻倍，時間翻倍；O(n²) 表示資料量翻倍，時間變成 4 倍。

**空間複雜度**：同樣用大 O 表示法。有些演算法用空間換時間（如雜湊表），有些用時間換空間（如壓縮演算法）。

**正確性**：演算法必須對所有可能的輸入都能給出正確結果。邊界條件（空輸入、極大輸入）最容易出錯。
:::

---

## 1. 二分搜尋：每次排除一半

### 1.1 二分搜尋的原理

::: tip 二分搜尋如何運作？
**前提**：資料必須有序

**過程**：
1. 找到中間元素
2. 如果中間元素等於目標，找到了！
3. 如果目標小於中間元素，在左半部分繼續
4. 如果目標大於中間元素，在右半部分繼續
5. 每次排除一半，直到找到或確定不存在

**時間複雜度**：O(log n)

**生活類比**：猜數字遊戲。我想一個 1-100 的數，你每次猜中間，我告訴你大了還是小了。最多猜 7 次就能猜中（因為 2⁷ = 128 > 100）。
:::

**動手試試看**：
下面這個演示展示了二分搜尋的運作原理，你可以選擇順序搜尋或二分搜尋來對比：

<SearchAlgorithmDemo />

### 1.2 二分搜尋的效能分析

| 資料量 | 線性搜尋 | 二分搜尋 |
|--------|---------|---------|
| 100 | 100 次 | 7 次 |
| 1,000 | 1,000 次 | 10 次 |
| 1,000,000 | 1,000,000 次 | 20 次 |
| 1,000,000,000 | 1,000,000,000 次 | 30 次 |

::: tip 逐行解讀這張表
**第一列（資料量）**：要搜尋的資料有多少。可以看到資料量從 100 增長到 10 億（擴大了 1000 萬倍！）

**第二列（線性搜尋）**：最「笨」的方法，從第一個開始一個一個找。搜尋次數等於資料量，資料量越大，搜尋次數越多。

**第三列（二分搜尋）**：聰明的方法，每次排除一半。搜尋次數只和資料量的對數有關，即使 10 億資料也只需要 30 次！

**對比結論**：當資料量達到 100 萬時，線性搜尋需要 100 萬次，二分搜尋只需要 20 次——差距達 5 萬倍！
:::

::: tip 對數增長的威力
二分搜尋的時間複雜度是 O(log n)，這意味著：

- 10 億資料，最多搜尋 30 次
- 1 兆資料，最多搜尋 40 次

這就是對數增長的威力——資料量增加 1000 倍，搜尋次數只增加 10 次。
:::

---

## 2. 排序：將無序變有序

### 2.1 常見排序演算法

| 演算法 | 時間複雜度 | 特點 | 適用場景 |
|------|-----------|------|---------|
| **氣泡排序** | O(n²) | 簡單但慢 | 教學、小資料量 |
| **選擇排序** | O(n²) | 簡單但慢 | 小資料量 |
| **插入排序** | O(n²) | 對近乎有序的資料快 | 小資料量、近乎有序 |
| **快速排序** | O(n log n) | 實際最快 | 通用排序 |
| **合併排序** | O(n log n) | 穩定排序 | 需要穩定性的場景 |
| **堆積排序** | O(n log n) | 原地排序 | 記憶體受限場景 |

::: tip 逐行解讀這張表
**氣泡排序**：最基礎的排序演算法，就像水底的氣泡往上冒一樣。簡單易懂，但速度最慢。適合學習排序思想，不適合實際使用。

**選擇排序**：每次選出最小的放到前面。也很簡單，但無論資料是否有序都要做同樣多的比較。

**插入排序**：像打撲克牌時整理手牌一樣。把每個元素插入到前面已經排好序的部分中。對近乎有序的資料效率很高。

**快速排序**：實際開發中最常用的排序。平均情況下最快，但最壞情況（資料已經有序）會退化到 O(n²)。

**合併排序**：採用「分而治之」的思想，總是 O(n log n)，但需要額外空間。適合需要穩定排序的場景。

**堆積排序**：利用堆積這種資料結構的排序，原地排序（不需要額外空間），但實際執行往往比快速排序慢。
:::

### 2.2 快速排序的效能分析

::: tip 快速排序的原理
**核心思想**：分治法

1. 選一個「基準」元素
2. 把比基準小的放左邊，比基準大的放右邊
3. 對左右兩部分遞迴排序
4. 合併結果

**優勢**：
- 每次劃分後，基準元素就到了最終位置
- 平均情況下，每次劃分大約排除一半元素
- 時間複雜度 O(n log n)

**生活類比**：整理書架。先抽出一本書，把比它薄的放左邊，比它厚的放右邊。然後對左右兩堆分別重複這個過程。
:::

**動手試試看**：
下面這個演示展示了排序演算法的視覺化，你可以產生陣列，觀察氣泡排序和快速排序的過程對比：

<SortingAlgorithmDemo />

---

## 3. 遞迴：自己呼叫自己

### 3.1 遞迴的本質

::: tip 遞迴概述
**遞迴**是函式呼叫自身的程式設計技巧。

**兩個關鍵要素**：
1. **基本情況**：什麼時候停止遞迴？
2. **遞迴步驟**：如何把問題分解成更小的子問題？

**經典例子：階乘**
```js
function factorial(n) {
  if (n <= 1) return 1        // 基本情況
  return n * factorial(n - 1) // 遞迴步驟
}
```

**生活類比**：俄羅斯套娃。打開一個娃娃，裡面是更小的娃娃，直到最小的那個打不開為止。
:::

### 3.2 遞迴 vs 迭代

| 特性 | 遞迴 | 迭代（迴圈） |
|------|------|-------------|
| **程式碼簡潔度** | 通常更簡潔 | 可能更複雜 |
| **記憶體消耗** | 較高（呼叫堆疊） | 較低 |
| **效能** | 稍慢（函式呼叫開銷） | 更快 |
| **適用場景** | 樹遍歷、分治演算法 | 簡單重複任務 |

::: tip 逐行解讀這張表
**程式碼簡潔度**：遞迴通常只需要幾行程式碼就能表達複雜的邏輯（如遍歷樹結構），而用迴圈可能需要更多的變數和巢狀。

**記憶體消耗**：遞迴會使用「呼叫堆疊」來儲存每一層的資訊，就像疊盤子一樣，每遞迴一層就多一個盤子。迴圈則不需要這種開銷。

**效能**：每次函式呼叫都有開銷（參數傳遞、堆疊操作等），所以遞迴通常比迴圈慢一些。

**適用場景**：遞迴擅長處理本身就是遞迴結構的問題（如檔案樹、DOM 樹）；迴圈擅長簡單的重複操作（如遍歷陣列）。
:::

::: warning 遞迴的陷阱
**堆疊溢位**：遞迴層次太深，呼叫堆疊空間耗盡。

**解決方法**：
- 改用迭代
- 使用尾遞迴最佳化（某些語言支援）
- 限制遞迴深度
:::

**動手試試看**：
下面這個演示展示了遞迴的呼叫過程，觀察函式如何自己呼叫自己：

<RecursiveThinkingDemo />

---

## 4. 貪婪演算法：每步選最優

### 4.1 貪婪的思想

::: tip 貪婪演算法概述
**貪婪演算法**在每一步都選擇當前看起來最優的選擇，希望最終得到全域最佳解。

**適用條件**：
1. **貪婪選擇性質**：區域最佳能導致全域最佳
2. **最佳子結構**：問題的最佳解包含子問題的最佳解

**經典例子：硬幣找零**
- 目標：用最少的硬幣湊出指定金額
- 貪婪策略：每次選最大的硬幣
- 結果：67 元 = 50 + 10 + 5 + 1 + 1（5 枚）

**生活類比**：登山時，每次都選最陡的路往上走。雖然不一定能到最高峰，但通常能到不錯的位置。
:::

### 4.2 貪婪的侷限性

::: warning 貪婪不一定得到最佳解
**反例：硬幣找零**

如果硬幣面值是 [1, 3, 4]，要湊 6 元：
- 貪婪：4 + 1 + 1 = 3 枚
- 最佳：3 + 3 = 2 枚

貪婪演算法在這裡失敗了！

**教訓**：貪婪演算法簡單高效，但不總是能得到最佳解。使用前要證明問題滿足貪婪條件。
:::

**動手試試看**：
下面這個演示展示了貪婪演算法的實際效果，你可以嘗試不同的硬幣組合，觀察貪婪策略的表現：

<GreedyThinkingDemo />

---

## 5. 演算法設計範式

| 範式 | 思想 | 典型演算法 | 適用問題 |
|------|------|---------|---------|
| **分治** | 把問題分解成小問題 | 快速排序、合併排序 | 可分解的問題 |
| **貪婪** | 每步選最優 | 最小生成樹、霍夫曼編碼 | 有貪婪性質的問題 |
| **動態規劃** | 記錄子問題的解 | 背包問題、最短路徑 | 有重疊子問題 |
| **回溯** | 試錯，走不通就退回 | 八皇后、全排列 | 搜尋問題 |

::: tip 逐行解讀這張表
**分治**：把大問題拆成小問題，分別解決後再合併。就像整理房間，先分成客廳、臥室、廚房分別打掃，最後整體整潔。

**貪婪**：每步都選當前最好的，不考慮長遠後果。像吃飯時先挑最喜歡吃的菜，可能不是最好的吃法，但速度快。

**動態規劃**：記住中間結果，避免重複計算。像記筆記，下次遇到同樣問題直接查答案，不用重新推導。

**回溯**：走不通就退回來重試。像走迷宮，此路不通就回到上一個路口嘗試別的路。
:::

**動手試試看**：
下面這個演示展示了不同演算法設計範式的特點和應用場景：

<AlgorithmParadigmDemo />

---

## 6. 總結：演算法是解決問題的藝術

讓我們用一個比喻總結各種演算法思想：

| 思想 | 比喻 | 核心要點 |
|------|------|---------|
| **二分搜尋** | 猜數字 | 每次排除一半 |
| **排序** | 整理書架 | 建立秩序 |
| **遞迴** | 俄羅斯套娃 | 化大為小 |
| **貪婪** | 登山選路 | 區域最佳 |

::: tip 核心啟示
**演算法的本質是「效率」和「正確性」的平衡。**

- 好的演算法能讓程式效率提升幾個數量級
- 但過度最佳化可能引入複雜性
- 先保證正確，再追求效率

理解演算法思維，比記住具體演算法更重要：
- 分治：把大問題分解成小問題
- 貪婪：每步選最優
- 動態規劃：記錄子問題的解
- 回溯：試錯，走不通就退回
:::

---

## 延伸閱讀

- **演算法導論**：系統學習演算法的經典教材
- **LeetCode**：透過刷題提升演算法能力
- **演算法視覺化**：直觀理解演算法執行過程
- **競賽演算法**：學習更高階的演算法技巧
