第 19 課 · 進階 · 20 分鐘

Python 遞迴

把問題交給一個更小版本的自己。

基線條件遞迴呼叫呼叫樹

推薦卡連成一條鏈:A 拉了 B,B 又拉了 C,後面還有。Maya 想知道每條鏈有多深,而不是一個個手數。遞迴在哪裡停下來?

遞迴函式會用更小的輸入呼叫自己。每個遞迴都需要一個能停止呼叫的基線條件,而且每一步都必須朝基線靠近。滿足這兩個條件時,遞迴可以把巢狀問題表達得更短。 你還會追蹤呼叫堆疊、攤平巢狀列表,並快取重複分支。

先寫基線條件

基線條件是不用再呼叫就能回答的最小輸入。factorial(1) 會立刻回傳 1。沒有基線條件,函式會一直呼叫自己,直到 Python 拋出 RecursionError。

def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)

print(factorial(5))

一次遞迴呼叫

兩次呼叫,更小的輸入

斐波那契天然是兩路遞迴:fib(n) 依賴 fib(n - 1) 和 fib(n - 2)。前兩個值是基線條件。畫出小 n 的呼叫樹,就能看出這個版本為什麼會重複計算。

def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(6))

分叉的遞迴

走訪巢狀列表

遞迴列表函式先處理第一個元素,再把剩下的列表交給更小的呼叫。空列表是基線條件。這個模式不只適用於平鋪列表,也適用於樹和巢狀結構。

def total(items):
    if not items:
        return 0
    return items[0] + total(items[1:])

print(total([1, 2, 3]))

不斷縮短的列表

呼叫堆疊會反向展開

每次遞迴呼叫都會暫停它的呼叫者。基線條件先回傳,然後每一層呼叫者恢復執行,組合回傳值。向下和向上各列印一次,就能看清最終結果為什麼是從裡往外建構的。

def walk(n):
    if n == 0:
        print("base")
        return 0
    print("enter", n)
    result = walk(n - 1) + n
    print("leave", n, "result", result)
    return result

print("total", walk(3))

向下進入,向上組合

回傳新結構,不要依賴全局變數

遞迴函式應該回傳自己的部分結果。全局累加器會讓多次呼叫互相干擾,也難以測試。攤平巢狀列表時,把第一個元素的結果和剩餘部分的結果組合起來。

def flatten(items):
    if not items:
        return []
    first, *rest = items
    if isinstance(first, list):
        return flatten(first) + flatten(rest)
    return [first] + flatten(rest)

print(flatten([1, [2, [3, 4]], 5]))

組合回傳值,而不是共用狀態

快取重複分支

樸素斐波那契會重複計算相同的子問題。lru_cache 按參數儲存結果,對這種形狀能把指數級工作變成線性級。只有純函式、且參數可雜湊時,快取才是安全的。

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(30))
print(fib.cache_info().hits > 0)

快取純函式的子問題

本課要點

  • 每個遞迴函式都需要基線條件。
  • 每次呼叫都必須使用更小或更簡單的輸入。
  • 遞迴呼叫回傳的值由呼叫者繼續組合。
  • 遞迴太深或重複太多時,迴圈可能更清楚。
  • 遞迴從內向外回傳;記憶化能去掉重複分支。

常見錯誤

  • 忘記基線條件,導致 RecursionError。
  • 用同樣的輸入呼叫函式,而不是縮小輸入。
  • 忽略分叉遞迴中的重複計算。
  • 快取會讀取變化中的全局狀態或修改輸入的函式。
輪到你

寫一個遞迴函式,把列表中的數字相加,然後列印 [2, 4, 6] 的結果。

動手試試

改改變數、先預測輸出,再執行程式碼。第一次點擊「執行程式碼」時會下載 Python 執行時,可能需要幾秒鐘。

瀏覽器內 Python本機執行 · 無需帳號
首次執行會從 CDN 下載 Pyodide。修改後點擊「執行程式碼」。

練習本課

12 道題,作答後立即看到解釋。

查看 12 道題答案開始練習
← lambda 與 map正規表達式 →