基礎程式設計(Java)· CH18

遞迴:方法呼叫自己

基底條件 · 遞迴步驟 · 展開與回溯 · 階乘 · 費氏 · 河內塔 · 遞迴 vs 迭代 · 導覽地圖

完整內容看 PDF 講義,動手追蹤與試跑看 index。

什麼是遞迴

把問題拆成更小的同類問題

大問題
factorial(4)
更小的同類
factorial(3)…
最簡單情況
factorial(1)=1

遞迴=方法在自己的方法體裡呼叫自己;每次都讓問題更接近最簡單的情況。

兩個必要部位

缺一不可

long factorial(int n) {
    if (n <= 1) return 1;             // ① 基底條件:終止點
    else return n * factorial(n - 1); // ② 遞迴步驟:縮小後呼叫自己
}

基底條件=可直接回答、不再呼叫自己;遞迴步驟=引數縮小一級、收斂到基底。

展開與回溯

下行拆解、上行回乘

展開 ↓
push 到
factorial(1)=1
回溯 ↑
pop,值往回乘
1→2→6→24

factorial(4) = 4*6 = 24。同一方法的多個活動記錄同時疊在堆疊上,各有自己的 n

呼叫堆疊

遞迴沒有魔法(同 CH6)

每次呼叫
push 活動記錄
到基底
不再 push
return
依序 pop(LIFO)

展開深度=同時疊的框數。漏基底/不收斂 → 疊到爆 → StackOverflowError

費氏數列

兩個基底、呼叫兩次

long fib(int n) {
    if (n == 0 || n == 1) return n;        // 兩個基底條件
    else return fib(n - 1) + fib(n - 2);   // 呼叫自己兩次
}
// 0 1 1 2 3 5 8 13 21 34

分叉呼叫樹,子問題被重算 → 呼叫次數指數成長,大 n 很慢。

河內塔

三步遞迴拆法

void hanoi(int n, char from, char to, char aux) {
    if (n == 1) { print(from + " -> " + to); }   // 基底
    else {
        hanoi(n-1, from, aux, to);   // ① n-1 片 → 輔助柱
        print(from + " -> " + to);   // ② 最大片 → 目標
        hanoi(n-1, aux, to, from);   // ③ n-1 片 → 目標
    }
}

n 片需 2ⁿ − 1 次搬移;hanoi(3) 共 7 步。

遞迴 vs 迭代

兩者可互換

遞迴
呼叫自己;基底條件終止;每次多一個堆疊框;貼近問題、易懂;太深會溢位。
迭代
迴圈重複;條件變 false 終止;不需額外框;省記憶體;單純累加/計數適用。

天然遞迴問題(樹、河內塔)用遞迴;線性累加用迭代更省。

陷阱

無限遞迴 → 溢位

long bad(int n) {
    return n * bad(n - 1);   // 缺基底條件!停不下來
}                            // → StackOverflowError

三個必檢點:有沒有基底條件引數有沒有朝基底收斂所有路徑都會抵達基底嗎

接下來

把遞迴練到能看穿堆疊

  • PDF 講義 完整內容+展開樹+呼叫堆疊圖
  • index factorial(4) 展開步進+試跑+遞迴vs迭代
  • 本地練習 5 題 階乘/費氏/求和/反轉/gcd
  • 複習測驗 自測到 90%