recursion 和 loop 分別適合什麽情景? - paulip114/blog GitHub Wiki
雖然在大多數情況下遞迴(recursion)都可以改寫成迴圈(loop),但還是有一些**「幾乎只能用 recursion」或「用 recursion 更自然」的情境**。
🧠 結論:幾乎所有遞迴都能轉成迴圈,但…
- 只有在需要隱式堆疊記憶結構時(如:函式呼叫自己、需記住多層狀態)→ 用遞迴最簡單。
- 某些語言支援 Tail Call Optimization(TCO) 時,遞迴比迴圈更高效。
- 部分資料結構或問題天然是樹狀、分支結構 → 轉成迴圈會很難讀或變成超麻煩的 Stack 模擬
✅ 情況一:樹或圖的遍歷(DFS)
範例情境:
- 遍歷 DOM 結構
- 遞迴印出檔案系統
- 樹狀節點的搜尋或統計
function printTree(node: TreeNode) {
console.log(node.value);
for (const child of node.children) {
printTree(child);
}
}
🌀 為什麼用 recursion?
你無法預先知道有幾層巢狀(可能 2 層,也可能 100 層),用 recursion 可以天然地讓呼叫堆疊幫你記住「我現在在哪一層」。
👉 可以用 Stack 實作 DFS,但程式碼更複雜、沒這麼直觀。
✅ 情況二:Backtracking(回朔法)
範例情境:
- N 皇后問題
- 組合 / 排列
- 數獨解題
- 解迷宮(找所有路徑)
function solve(i: number, path: number[]) {
if (path.length === target) {
result.push([...path]);
return;
}
for (let j = i; j < nums.length; j++) {
path.push(nums[j]);
solve(j + 1, path);
path.pop(); // 回朔
}
}
🌀 為什麼用 recursion?
因為每一次選擇都可能分成新的子問題,而每層狀態都需要被記住才能「回朔」,用 recursion + stack 很自然。
👉 如果用迴圈寫,要手動維護「狀態堆疊」,超麻煩。
✅ 情況三:遞迴定義的數學問題(天然遞迴)
範例情境:
- 費氏數列(Fibonacci)
- Factorial(階乘)
- 分數化簡
- 某些組合數 / 數學歸納
function factorial(n: number): number {
if (n === 0) return 1;
return n * factorial(n - 1);
}
👉 雖然可以用迴圈寫,但 recursion 會更貼近數學定義,更簡潔。
✅ 情況四:語言處理 / 編譯器 / 表達式解析
範例情境:
- 解析
(3 + (2 * 5))
→ 計算器 - 處理巢狀語法(像 JSON parser)
- 解釋器、語法分析器(Parser)
// 如果是 token stream,有巢狀括號,就很適合遞迴
function parseExpression(tokens: string[]): Node {
// 處理 ( ) 巢狀遞迴
}
👉 遞迴呼叫自己可以自然處理每一層表達式,不需自己維護巢狀堆疊。
❌ 哪些情況不該用 recursion?
- 太深的遞迴會爆堆疊(stack overflow)
- 特別是你沒有 tail call 優化的語言(JS 沒支援 TCO)
- 建議改成 loop + stack 實作(像 DFS)
- 明確知道要重複 N 次或固定結構
- 比如跑 1000 次、陣列跑 for loop → 用迴圈就好
🔚 小結:什麼時候只能用(或最好用)遞迴?
情境 | 原因 |
---|---|
樹狀或巢狀結構(DOM、檔案系統) | 不確定深度,天然遞迴 |
Backtracking(N 皇后、排列) | 每層選擇要記住狀態,回朔用 stack 處理麻煩 |
數學定義(factorial、fib) | 遞迴對照數學公式,簡單清楚 |
語法解析(JSON、表達式) | 巢狀語法遞迴下降(Recursive Descent Parser) |
DFS 遍歷(進階圖論) | 可用 loop + stack,但遞迴更直觀 |