演算法系列 · 第 17 關 · 遞迴與進階

遞迴 Recursion

函式自己呼叫自己,把大問題拆成同樣形狀的小問題。分治、回溯、DP 的共同地基。

遞迴就是函式自己呼叫自己,把一個大問題拆成「同樣形狀但更小」的問題,一路拆到簡單到不用再拆為止。每個遞迴都要有兩塊:終止條件(base case)讓它停下來,和遞迴步驟把問題縮小、往終止條件靠近。它是分治、回溯、動態規劃的共同基礎。

遞迴的兩塊(缺一不可)

base case(終止條件) —— 小到不用再拆的情況,直接給答案(例如 factorial(1) = 1)。這是「煞車」。

遞迴步驟(recursive case) —— 用「更小的自己」來解,並確保每次都往 base case 靠近。

少了 base case —— 停不下來,直接 RecursionError(堆疊爆掉)。

問題沒有變小 —— 一樣停不下來。每次呼叫,參數一定要更接近終止條件。

呼叫堆疊:遞迴其實在疊盤子

每次呼叫都「暫停自己、等子呼叫」 —— 這些等待中的呼叫,一層層疊在呼叫堆疊上。

一路拆到 base case(往下疊) —— 這叫「遞」。

從 base case 開始一層層回傳(往上收) —— 這叫「迴」。

所以遞迴跟堆疊是一體兩面 —— 就是「堆疊」那頁講的 call stack。下面的視覺化就是在演這個。

遞迴 vs 迴圈(迭代)

樹狀 / 巢狀 / 分治問題 —— 遞迴通常更短、更貼近問題本身(走訪樹、全排列、河內塔)。

單純線性重複 —— 用迴圈往往更快、更省記憶體(不會一直堆呼叫)。

遞迴的代價 —— 每層呼叫有開銷,還可能堆疊爆掉(Python 遞迴上限約 1000)。

有些遞迴會重複算 —— 費式數列直接遞迴會爆炸,要加記憶化把算過的存起來(範例 4,DP 的地基)。

複雜度(看是哪種遞迴)

O(n)線性遞迴 —— 每次縮小一點、疊 n 層(factorial、sum、反轉)。
O(2ⁿ)樹狀遞迴、沒記憶化 —— 每層分岔成多個子呼叫,重複計算爆炸(天真費式數列)。
O(深度)空間 —— 呼叫堆疊最深 = 遞迴深度,這是遞迴比迴圈多花的成本。
遞迴的快慢不是「遞迴」本身決定的,而是「它把問題拆成幾個、有沒有重複」。線性拆是 O(n)、砍半拆是 O(log n)、無腦分兩岔又不記憶是 O(2ⁿ)。

用生活比喻它

🪆

俄羅斯娃娃

打開一個,裡面還有更小的一個,一直到最小那個(base case),再一層層蓋回去。

🎬

電影裡的電影

片中有人在看電影,那部裡又有人在看…看到最裡面那部演完,才一層層回到最外面。

📁

資料夾裡的資料夾

打開一層點進下一層,點到最底沒有子資料夾了,再一層層退回來。

🎮 遞迴呼叫堆疊(factorial)

看 factorial(4) 怎麼跑:先一路呼叫更小的自己(遞,堆疊往下疊),碰到 factorial(1) 這個 base case 就回傳,再一層層把答案乘回去(迴,堆疊往上收)。紫色是正在動的那層、綠色是已回傳、琥珀是還在等子呼叫。

10 個程式碼範例(Python)

從階乘、累加、費式數列(天真 vs 記憶化),到快速冪、反轉、河內塔、攤平巢狀、遞迴二分搜尋。

顯示範例:

常見陷阱

❌ 忘了 base case —— 停不下來,RecursionError(堆疊爆掉)。

❌ 遞迴沒讓問題變小 —— 每次呼叫的參數要更接近 base case,不然一樣停不下來。

❌ 天真遞迴重複算 —— 費式數列這種要加記憶化,不然 O(2ⁿ)。

❌ 遞迴太深 —— 資料很深(幾萬層)會撞上限;改用迴圈 + 自己的堆疊,或 sys.setrecursionlimit 調高。

🎯 換你了(先別急著查答案)

把答案打到對話裡,我幫你對。