Python 基礎:遞迴函數的完整教學
Python 基礎:遞迴函數的完整教學
遞迴(Recursion)是電腦科學中一個非常重要的概念,也是一種很強大的程式設計技術。簡單來說,遞迴就是函數呼叫自己的過程。本篇文章將帶你深入了解遞迴的原理與應用,並特別說明全域變數與區域變數在遞迴中的使用方式,以及常見的錯誤與重點整理。
1. 遞迴的基本概念
1.1 什麼是遞迴?
遞迴是指函數在執行過程中直接或間接呼叫自己的現象。遞迴讓我們可以用簡潔的方式解決複雜的問題,特別是當問題可以分解為相同性質的子問題時。
讓我們用生活中的例子來理解:
從前有座山,山裡有座廟,廟裡有個老和尚,老和尚在給小和尚講故事:從前有座山,山裡有座廟…
這就是一個無限遞迴的例子。當然,我們的程式需要有結束條件,否則會陷入無限迴圈。
1.2 遞迴的組成要件
一個正確的遞迴函數必須包含兩個部分:
- 基本案例(Base Case):遞迴的終止條件,當滿足這個條件時不再繼續遞迴。
- 遞迴案例(Recursive Case):將問題分解為更小的子問題,繼續呼叫自己。
1
2
3
4
5
6
7
8
def 遞迴範例(n):
if n <= 0: # 基本案例:終止條件
return
print(n)
遞迴範例(n - 1) # 遞迴案例:呼叫自己
遞迴範例(5)
# 輸出:5, 4, 3, 2, 1
1.3 遞迴的呼叫過程
讓我們用更詳細的例子來看遞迴是如何執行的:
1
2
3
4
5
6
7
8
9
10
def 計算階乘(n):
print(f"進入函數:n = {n}")
if n == 1: # 基本案例
print(f"達到基本案例,返回 1")
return 1
結果 = n * 計算階乘(n - 1) # 遞迴呼叫
print(f"返回結果:{結果}")
return 結果
計算階乘(3)
執行過程:
1
2
3
4
5
6
進入函數:n = 3
進入函數:n = 2
進入函數:n = 1
達到基本案例,返回 1
返回結果:2
返回結果:6
2. 經典遞迴範例
2.1 計算階乘
階乘(Factorial)是指一個正整數與它以下所有正整數的乘積,記作 n!。
1
2
3
4
5
6
7
def 階乘(n):
if n <= 1: # 基本案例
return 1
return n * 階乘(n - 1) # 遞迴案例
print(階乘(5)) # 輸出:120
print(階乘(0)) # 輸出:1(0! = 1)
2.2 費波那契數列
費波那契數列(Fibonacci Sequence)是這樣的數列:1, 1, 2, 3, 5, 8, 13, 21, …
每一個數都是前兩個數的和。
1
2
3
4
5
6
7
8
def 費波那契(n):
if n <= 1: # 基本案例
return n
return 費波那契(n - 1) + 費波那契(n - 2) # 遞迴案例
for i in range(10):
print(費波那契(i), end=" ")
# 輸出:0 1 1 2 3 5 8 13 21 34
2.3 計算次方
1
2
3
4
5
6
7
def 次方(base, exponent):
if exponent == 0: # 基本案例
return 1
return base * 次方(base, exponent - 1)
print(次方(2, 10)) # 輸出:1024
print(次方(5, 3)) # 輸出:125
2.4 反轉字串
1
2
3
4
5
6
7
def 反轉字串(s):
if len(s) <= 1: # 基本案例
return s
return s[-1] + 反轉字串(s[:-1])
print(反轉字串("Hello")) # 輸出:olleH
print(反轉字串("Python")) # 輸出:nohtyP
2.5 計算陣列總和
1
2
3
4
5
6
def 陣列總和(arr):
if len(arr) == 0: # 基本案例
return 0
return arr[0] + 陣列總和(arr[1:])
print(陣列總和([1, 2, 3, 4, 5])) # 輸出:15
2.6 二元搜尋
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def 二元搜尋(arr, target, left, right):
if left > right: # 基本案例:找不到
return -1
mid = (left + right) // 2
if arr[mid] == target:
return mid # 找到目標
elif arr[mid] < target:
return 二元搜尋(arr, target, mid + 1, right) # 搜尋右半邊
else:
return 二元搜尋(arr, target, left, mid - 1) # 搜尋左半邊
arr = [1, 3, 5, 7, 9, 11, 13, 15]
print(二元搜尋(arr, 7, 0, len(arr) - 1)) # 輸出:3
print(二元搜尋(arr, 6, 0, len(arr) - 1)) # 輸出:-1
3. 遞迴與迴圈的比較
3.1 兩種方式的比較
很多遞迴問題也可以用迴圈(Loop)來解決:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 用迴圈計算階乘
def 階乘_迴圈(n):
result = 1
for i in range(1, n + 1):
result *= i
return result
# 用遞迴計算階乘
def 階乘_遞迴(n):
if n <= 1:
return 1
return n * 階乘_遞迴(n - 1)
print(階乘_迴圈(5)) # 輸出:120
print(階乘_遞迴(5)) # 輸出:120
3.2 遞迴的優點
- 程式碼簡潔:用更少的程式碼表達複雜邏輯
- 自然表達:某些問題用遞迴表達更直觀(如樹結構遍歷)
- 易於理解:符合人類思考方式
3.3 遞迴的缺點
- 記憶體消耗:每次遞迴呼叫都需要佔用記憶體空間
- 效能較慢:函數呼叫有額外開銷
- 可能發生棧溢取:過深的遞迴會導致堆疊溢出
4. 全域變數與區域變數在遞迴中的應用
4.1 區域變數在遞迴中的使用
在遞迴函數中,區域變數會在每次呼叫時創建新的副本,這是遞迴正確運作的關鍵:
1
2
3
4
5
6
7
8
9
10
def 遞迴與區域變數(n):
計數 = 0 # 區域變數,每次呼叫都會重新創建
計數 += 1
print(f"n={n}, 計數={計數}")
if n > 0:
遞迴與區域變數(n - 1)
遞迴與區域變數(3)
# 每次呼叫的計數都是 1
4.2 全域變數在遞迴中的使用
使用全域變數需要注意,遞迴中的修改會影響全域狀態:
1
2
3
4
5
6
7
8
9
10
11
12
計數 = 0 # 全域變數
def 遞迴與全域變數(n):
global 計數
計數 += 1
print(f"n={n}, 計數={計數}")
if n > 0:
遞迴與全域變數(n - 1)
遞迴與全域變數(3)
print(f"最終計數:{計數}") # 輸出:4
4.3 使用區域變數避免副作用
建議:盡量使用區域變數,避免使用全域變數造成副作用:
1
2
3
4
5
6
7
8
9
10
def 安全的遞迴(n):
# 使用參數傳遞,避免全域變數
def 內部遞迴(n, 計數):
if n == 0:
return 計數
return 內部遞迴(n - 1, 計數 + 1)
return 內部遞迴(n, 0)
print(安全的遞迴(3)) # 輸出:3
4.4 使用列表收集結果
1
2
3
4
5
6
7
8
9
10
11
12
13
def 收集所有結果(n):
結果列表 = [] # 區域變數
def 遞迴收集(i):
if i == 0:
return
結果列表.append(i) # 修改外層函數的區域變數
遞迴收集(i - 1)
遞迴收集(n)
return 結果列表
print(收集所有結果(5)) # 輸出:[5, 4, 3, 2, 1]
5. 常見錯誤與解決方案
5.1 忘記基本案例
錯誤範例:
1
2
3
4
5
def 無限遞迴(n):
return n + 無限遾迴(n - 1) # 沒有終止條件,會無限遙迴
# 執行這行會導致程式崩潰
# print(無限遞迴(5))
解決方案:務必設定基本案例
1
2
3
4
5
6
def 正確的遞迴(n):
if n <= 0: # 基本案例
return 0
return n + 正確的遾迴(n - 1)
print(正確的遙迴(5)) # 輸出:15
5.2 基本案例撰寫錯誤
錯誤範例:
1
2
3
4
def 錯誤的費波那契(n):
if n == 0: # 錯誤的基本案例
return 1
return 錯誤的費波那契(n - 1) + 錯誤的費波那契(n - 2)
解決方案:正確的基本案例
1
2
3
4
def 正確的費波那契(n):
if n <= 1: # 正確:n=0 和 n=1 都是基本案例
return n
return 正確的費波那契(n - 1) + 正確的費波那契(n - 2)
5.3 遞迴深度過深
錯誤範例:
1
print(遞迴(10000)) # RecursionError: maximum recursion depth exceeded
解決方案:增加遙迴限制或改用迴圈
1
2
3
4
5
6
7
8
9
10
11
12
13
import sys
# 增加遙迴限制(不建議無限制增加)
sys.setrecursionlimit(10000)
# 或改用迴圈
def 迴圈版本(n):
if n <= 1:
return n
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
5.4 全域變數污染
錯誤範例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
計數 = 0
def 遙迴(n):
global 計數
if n == 0:
return
計數 += 1
遙迴(n - 1)
遙迴(5)
print(計數) # 輸出:5
# 再次呼叫會累加
遙迴(5)
print(計數) # 輸出:10(出錯!)
解決方案:使用參數傳遞
1
2
3
4
5
6
7
def 正確的遙迴(n, 計數=0):
if n == 0:
return 計數
return 正確的遙迴(n - 1, 計數 + 1)
print(正確的遙迴(5)) # 輸出:5
print(正確的遙迴(5)) # 輸出:5(正確)
5.5 效能問題:重複計算
錯誤範例:
1
2
3
4
5
6
7
8
# 費波那契的效率問題
def 費波那契(n):
if n <= 1:
return n
return 費波那契(n - 1) + 費波那契(n - 2)
# 這個函數的複雜度是指數級 O(2^n)
# 計算費波那契(40) 需要很長時間
解決方案:使用記憶化(Memoization)
1
2
3
4
5
6
7
8
9
快取 = {}
def 費波那契優化(n):
if n in 快取:
return 快取[n]
if n <= 1:
return n
快取[n] = 費波那契優化(n - 1) + 費波那契優化(n - 2)
return 快取[n]
6. 遞迴的實際應用
6.1 目錄遍歷
1
2
3
4
5
6
7
8
9
10
11
12
import os
def 遍歷目錄(path, indent=0):
items = os.listdir(path)
for item in items:
full_path = os.path.join(path, item)
print(" " * indent + item)
if os.path.isdir(full_path):
遍歷目錄(full_path, indent + 1)
# 遍歷當前目錄
遍歷目錄(".")
6.2 解決河內塔問題
1
2
3
4
5
6
7
8
9
10
def 河內塔(n, 來源, 目標, 輔助):
if n == 1:
print(f"移動盤子 1 從 {來源} 到 {目標}")
return
河內塔(n - 1, 來源, 輔助, 目標)
print(f"移動盤子 {n} 從 {來源} 到 {目標}")
河內塔(n - 1, 輔助, 目標, 來源)
# 三根柱子:A、B、C
河內塔(3, 'A', 'C', 'B')
6.3 生成所有排列組合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def 全排列(arr):
if len(arr) <= 1:
return [arr]
結果 = []
for i in range(len(arr)):
當前 = arr[i]
剩餘 = arr[:i] + arr[i+1:]
for p in 全排列(剩餘):
結果.append([當前] + p)
return 結果
print(全排列([1, 2, 3]))
# 輸出:[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
7. 重點整理
7.1 遞迴的核心概念
- 基本案例(Base Case):遞迴的終止條件
- 遞迴案例(Recursive Case):將問題分解為子問題
- 呼叫堆疊:每次遞迴呼叫都會佔用記憶體
7.2 常見的遞迴應用
- 階乘計算
- 費波那契數列
- 反轉字串
- 二元搜尋
- 樹結構遍歷
7.3 遞迴的優缺點
優點:
- 程式碼簡潔
- 符合人類思考方式
- 適合解決可分解的問題
缺點:
- 記憶體消耗大
- 可能發生堆疊溢出
- 效能可能較差
7.4 全域變數與區域變數
- 區域變數:每次遞迴呼叫都會創建新的副本
- 全域變數:在遞迴中修改會影響全域狀態
- 建議:盡量使用區域變數和參數傳遞
7.5 常見錯誤提醒
- 務必設定基本案例
- 注意遞迴深度限制
- 避免全域變數污染
- 留意效能問題,使用記憶化優化
8. 結論
遞迴是程式設計中非常重要的概念,透過本篇文章,我們學習了:
- 遞迴的基本概念與組成要件
- 經典的遞迴範例(階乘、費波那契等)
- 遞迴與迴圈的比較
- 全域變數與區域變數在遞迴中的應用
- 常見錯誤與解決方案
- 遞迴的實際應用
掌握遙迴技巧後,你可以更優雅地解決許多複雜的問題。建議多加練習,並嘗試將一些用迴圈實現的程式改用遞迴來實現,加深對遞迴概念的理解。
持續練習,你會發現遞迴是一個非常強大的工具!