① 照節次讀:每一節先讀說明、動手玩互動元件,預測結果再按按鈕驗證。 ② 對照講義:頁上 §徽章對應 cppds 章節;細節與完整程式請回講義 06 與 cppds 原文。 ③ 每節做 quiz:答錯就回到該節重讀,不要往下跳。 ④ 最後翻 關鍵詞彙卡(7 張)自測術語(中文為主、術語附英文),並用 REF 總覽區當速查表。標「講義補充」的節是課堂沒細講的延伸,第一輪可略過。
遞迴(recursion)= 函式呼叫自己,每次都處理更小的同型問題,
直到小到可以直接回答(base case)。
sum([1,3,5,7,9]) 其實就是 1 + sum([3,5,7,9]) :括號一路往內,再一路往外收。
int f(int n) {{ return n + f(n - 1); }} 違反了哪條法則?執行會怎樣?
第 3 章用 stack 反轉餘數順序;遞迴版連 stack 都不用自己管: 「先遞迴處理商、回來再接上自己的餘數」的順序天然就是高位在前。因為 call stack 就是那個 stack。
講義在這裡留了一題填空:寫一個遞迴的 reverse(s),讓
reverse("hello") 回傳 "olleh"。原題挖掉兩個空,解答是:
用三法則檢查:base case 是長度不超過 1;
s.substr(1) 每次少一個字元,確實朝 base case 前進;而且它呼叫自己。
跟 toStr 同一個骨架:把「接字元」延後到遞迴回來之後,順序就自動反了。
把最後一行改成 return s[0] + reverse(s.substr(1));,會得到什麼?
每次函式呼叫,系統就在 call stack 推入一個 stack frame(區域變數+返回位址)。 遞迴深 n 層 = 同時存在 n 個 frame。return 就是 pop:把值交回上一層 frame 停住的位置。
5AD
這版沒有遞迴:自己開一個 stack 存餘數字元、最後倒出來。它證明了一件事:遞迴版其實是把同一個 stack 藏進了「呼叫堆疊」,每一層呼叫的區域變數就是一格 stack frame。
depth=1, n=10 depth=2, n=5 depth=3, n=2 depth=4, n=1 1010
深度一路長到 4:n 每除一次 2 就多一層 frame。最深那層(n=1)先回傳「1」,然後一路「回程」把餘數黏在後面,1010 是回程時由左往右組出來的。
課堂投影片跳過本節(RISE skip):屬自學補充。
遞迴難學,常常是因為腦中缺一張圖。cppds 的 C++ 範例使用 CTurtle.hpp;這裡用 canvas 重現同一套遞迴視覺,不需要額外安裝圖形函式庫。
先看螺旋:每一步往前畫一段線、右轉 90 度,然後用短一點的長度呼叫自己。 長度歸零就是 base case。
課堂投影片跳過本節(RISE skip):屬自學補充。
手繪流程只有三句話:畫一個大三角形;連接三邊中點,得到四個小三角形; 中間那個不管,對三個角落的小三角形重複同樣程序。 base case 是人為設定的degree(碎形深度):每遞迴一層減 1,減到 0 就停。
degree = 3 時,sierpinski 總共呼叫了幾次(含最外層那一次)?
把 n 個盤子從 A 搬到 C(大盤不能壓小盤):相信 n−1 盤的搬法已存在, 那 n 盤 = 先把 n−1 盤挪到中繼柱、搬最大盤、再把 n−1 盤疊回來。移動次數 $2^n - 1$。
moving disk from A to B moving disk from A to C moving disk from B to C moving disk from A to B moving disk from C to A moving disk from C to B moving disk from A to B
7 行輸出 = 2³ − 1 步,跟理論下限一模一樣。注意基底情況是「height < 1 什麼都不做」:它藏在 if 的反面,這種「隱形 base case」是遞迴的常見寫法。拿上面的互動動畫對照,每一行輸出對應一次圓盤移動。
希臘神話裡忒修斯進迷宮殺牛頭人,靠一團線找到回頭路。我們的演算法用的是另一個道具: 麵包屑。從起點問「往北一步之後,能走出去嗎」,這又是一個一模一樣的迷宮問題, 於是遞迴。北邊不行換南邊,再換西邊、東邊。走過的格子撒麵包屑,下次踩到就立刻回頭, 不然兩格之間會互相呼叫到天荒地老。回溯(backtrack)在程式裡就一件事:從遞迴呼叫 return。
四個 base case 要背起來:撞牆、踩到麵包屑(走過或死路)、到達邊緣(出口)、四個方向全部失敗。
++++++++++++++++++++++ +-OO+OOO++ ++ + OOOOOO+OOO ++++++++++ +-+-OO ++O ++++ +++ ++ +-+-OO+ +O++ OO +++ + +---OO OO++OO++ + + +++++-+ +-OOOOO++ + + +++++-+++--+-+OO++ + +----------+-+ O+ + + +++++-+--+-+-+ + + ++++++++++++++++++++++
O 是最後成功的那條路,− 是「試過但退回」的死路格子:遞迴回溯的痕跡全印在圖上。四個方向的 || 短路串接讓「找到一條就收工」,找不到才會把整片區域踩成 −。
課堂投影片跳過本節(RISE skip,講義也標 Optional):屬自學補充,但 DP 是後續演算法課的重要橋段。
自動販賣機要用最少的硬幣找零。37 分怎麼找?兩個 25 太多,一個 25、一個 10、兩個 1,共四枚。 這種「先拿最大的」叫貪婪法,美國硬幣(1、5、10、25)下它剛好都對。 但假設某國多了一種 21 分硬幣:63 分的最佳解是三枚 21,貪婪法卻還是給你六枚(25+25+10+1+1+1)。 貪婪法死在「當下最好」不等於「整體最好」。
為了比較計算量,cppds 接下來三個版本固定使用四種硬幣 {1,5,10,25},目標為 63 分。
63 分的最少硬幣數,是「拿一枚 1 分之後 62 分的最少」「拿一枚 5 分之後 58 分的最少」……
之中最小的那個加一。這個定義完全正確,卻慢得離譜:天真遞迴要 67,716,925 次呼叫,
因為同樣的子問題(例如 15 分)被翻來覆去重算了幾十次。講義給了三帖藥,一帖比一帖徹底:
| 版本 | 策略 | 63 分的呼叫數 |
|---|---|---|
| makeChange1 | 天真遞迴;coins={1,5,10,25} | 67,716,925 |
| makeChange2 | memoization;同一組四硬幣 | 221(cppds 此版) |
| makeChange3 | bottom-up DP;同一組四硬幣 | 至多 63 × 4 次硬幣檢查 |
memoization(也叫 caching)已經把爆炸壓下來了,但表裡還有洞、邏輯像補丁。 真正的 DP 換一個角度:從 1 分開始,由小到大把每個金額的最佳解填進表格。 填到第 c 分時,所有更小的金額都已經有答案,每格只要看「扣掉一枚硬幣後查表」哪個最小。 下方 23 分互動才另外加入 21 分硬幣,用來重現前面的貪婪反例。
天真遞迴解 63 分要約 6,772 萬次呼叫;DP 只需逐格檢查硬幣。DP 快的核心原因是?
toStr(10, 2) 的回傳值是?(動畫可驗證)
4 盤河內塔最少要移動幾次?其中「最大盤」移動幾次?
迷宮遞迴少寫「走過的格子直接 return false」會怎樣?
| 問題 | base case | 縮小方式 | 複雜度 |
|---|---|---|---|
| listSum | 索引到 vector 尾端 | 索引加 1 | O(n) |
| toStr(n, base) | n < base | n / base | O(log n) |
| 河內塔 | height < 1 | height − 1(兩次) | O(2ⁿ) |
| 迷宮 | 牆/走過/出口 | 相鄰格子 | O(格子數) |
| 找零錢(DP) | 金額 0 | 金額 − 硬幣面額 | O(金額×硬幣種) |
詞彙卡取自本章課程題庫,已譯為繁體中文,正面附英文原名。先看正面術語,心中默想定義再翻面對答案;洗牌後再過一輪,直到每張都能不假思索說出來。