遞迴(Recursion)#
遞迴是一種函式自己呼叫自己的程式設計技巧。從演算法思想來看,遞迴是一種特殊的迴圈,透過函式呼叫的堆疊(Stack)來實現重複執行。
遞迴的本質#
去的過程叫「遞」,回來的過程叫「歸」。遞迴將問題分解為更小規模的子問題,直到達到終止條件,再將結果層層返回。
經典比喻:電影院找座位#
你在電影院想知道自己在第幾排,但太暗看不清。於是你問前面的人,前面的人也問他前面的人,直到第一排的人說「我在第一排」,答案再一層層傳回來。
f(n) = f(n-1) + 1,其中 f(1) = 1遞迴三條件#
只要同時滿足以下三個條件,就可以用遞迴來解決:
- 問題可分解為子問題 - 子問題是資料規模更小的同類問題
- 求解思路完全相同 - 問題與子問題除了資料規模不同,處理方式一樣
- 存在終止條件 - 必須有停止遞迴的邊界條件
遞迴程式碼模板#
flowchart TD
A[進入遞迴] --> B{1. 終止條件<br/>level > MAX?}
B -->|是| C[處理結果並返回]
B -->|否| D[2. 處理當前層邏輯]
D --> E[3. 下探到下一層<br/>recursion level+1]
E --> F[4. 清理當前層狀態<br/>回溯恢復]
F --> G[返回上一層]
style B fill:#fff3e0
style C fill:#e8f5e9
style E fill:#e3f2fd
style F fill:#fce4ecdef recursion(level, param1, param2, ...):
# 1. Recursion Terminator (終止條件) - 最優先編寫
if level > MAX_LEVEL:
process_result()
return
# 2. Process Logic (處理當前層邏輯)
process(level, data...)
# 3. Drill Down (下探到下一層)
recursion(level + 1, p1, ...)
# 4. Reverse State (清理當前層狀態,如需要)
# 例如:恢復全域變數、回溯法中的路徑重置寫遞迴程式碼的關鍵:寫出遞推公式,找到終止條件。不要試圖用人腦展開每一層遞迴,只需思考當前層與下一層的關係。
經典案例:爬樓梯#
假設有 n 個台階,每次可以跨 1 或 2 個台階,問有多少種走法?
遞推公式:
f(1) = 1
f(2) = 2
f(n) = f(n-1) + f(n-2)程式碼實作
def climb_stairs(n):
if n == 1: return 1
if n == 2: return 2
return climb_stairs(n-1) + climb_stairs(n-2)遞迴的常見問題#
1. 堆疊溢出(Stack Overflow)#
函式呼叫會使用栈來保存臨時變數,遞迴深度過深會導致堆疊溢出。
解決方案:限制遞迴深度
depth = 0
def f(n):
global depth
depth += 1
if depth > 1000:
raise Exception("遞迴深度超限")
# ...2. 重複計算#
以費氏數列為例,計算 f(6) 時,f(4) 會被計算多次。
含有大量重複子問題的樸素遞迴,時間複雜度往往是指數級 O(2^n)。
解決方案:記憶化(Memoization)
memo = {}
def fib(n):
if n in memo:
return memo[n]
if n <= 2:
return 1
memo[n] = fib(n-1) + fib(n-2)
return memo[n]3. 其他問題#
- 時間效率:函式呼叫本身有開銷
- 空間複雜度:遞迴呼叫需要保存現場資料,空間複雜度為 O(n)
遞迴轉迭代#
所有遞迴理論上都可以改寫為迭代(迴圈),但在樹、圖的走訪中,遞迴提供了更好的可讀性。
爬樓梯的迭代版本
def climb_stairs_iterative(n):
if n == 1: return 1
if n == 2: return 2
prepre, pre = 1, 2
for i in range(3, n + 1):
current = pre + prepre
prepre = pre
pre = current
return pre實戰技巧#
不要人肉遞迴! 初學者常嘗試在腦中模擬每一層的執行,這很容易「爆腦」。應關注當前層的邏輯以及與下一層的介面,相信子問題已經被正確解決。