用遞迴整理水井村文化資產與導覽路線
當地方資料像樹枝一樣層層展開,讓函式走進每一個分支,再帶著答案回來。
完成本篇後,你能辨認「基底條件」與「遞迴步驟」,讀懂呼叫堆疊,並以遞迴列印、統計及搜尋巢狀資料;也能判斷何時應改用迴圈。
一、為什麼地方資料會需要遞迴?
一個導覽主題下面可能有多個分類,每個分類又包含景物、故事或產業節點。層數不固定時,預先寫三層、四層迴圈既笨重又容易漏掉。遞迴的想法是:把大問題拆成結構相同的小問題,交給同一個函式處理。
二、遞迴的兩個必要零件
| 零件 | 作用 | 缺少時的結果 |
|---|---|---|
| 基底條件 | 告訴函式何時停止 | 無限呼叫,最後出現 RecursionError |
| 遞迴步驟 | 把問題縮小後再次呼叫自己 | 問題沒有被逐步解決 |
先用倒數暖身。每次呼叫都把 number 減少 1,直到 0 為止。
def countdown(number):
if number == 0: # 基底條件
print("巡查開始!")
return
print(number)
countdown(number - 1) # 問題縮小
countdown(3)呼叫順序是 countdown(3) → countdown(2) → countdown(1) → countdown(0)。函式抵達基底條件後,再逐層返回原本暫停的位置;這些等待中的呼叫保存在「呼叫堆疊」。
三、建立水井村樹狀教學資料
每個節點都是字典,具有「名稱」與「子項目」。葉節點的子項目是空串列,因此同一個函式可以處理所有層級。
cultural_tree = {
"名稱": "水井村",
"子項目": [
{
"名稱": "地方故事",
"子項目": [
{"名稱": "白馬", "子項目": []},
{"名稱": "烏龜", "子項目": []},
{"名稱": "姻緣花", "子項目": []},
],
},
{
"名稱": "生活與產業",
"子項目": [
{"名稱": "柴井車", "子項目": []},
{"名稱": "智慧養殖", "子項目": []},
{"名稱": "魚菜共生", "子項目": []},
],
},
],
}四、任務一:展開巢狀導覽清單
def print_tree(node, depth=0):
indent = " " * depth
print(f"{indent}• {node['名稱']}")
for child in node["子項目"]:
print_tree(child, depth + 1)
print_tree(cultural_tree)當 子項目 是空串列時,for 不會執行,函式自然返回。這裡的空串列就是隱含的基底條件。
五、任務二:統計全部節點與導覽點
目前節點先算 1,再加上所有子樹的節點數。若只想統計沒有子項目的導覽點,遇到葉節點時回傳 1。
def count_nodes(node):
total = 1
for child in node["子項目"]:
total += count_nodes(child)
return total
def count_leaves(node):
if not node["子項目"]:
return 1
return sum(count_leaves(child) for child in node["子項目"])
print("全部節點:", count_nodes(cultural_tree))
print("導覽點:", count_leaves(cultural_tree))六、任務三:搜尋名稱並回傳完整路徑
搜尋不只要知道「找到了」,還要回傳從村落根節點走到目標的路徑。注意參數預設值使用 None,避免不同呼叫共用同一個串列。
def find_path(node, target, path=None):
if path is None:
path = []
current_path = path + [node["名稱"]]
if node["名稱"] == target:
return current_path
for child in node["子項目"]:
result = find_path(child, target, current_path)
if result is not None:
return result
return None
path = find_path(cultural_tree, "魚菜共生")
if path:
print(" → ".join(path))
else:
print("找不到指定導覽點")七、把葉節點整理成導覽選單
def collect_routes(node, path=None):
if path is None:
path = []
current_path = path + [node["名稱"]]
if not node["子項目"]:
return [" → ".join(current_path)]
routes = []
for child in node["子項目"]:
routes.extend(collect_routes(child, current_path))
return routes
for number, route in enumerate(collect_routes(cultural_tree), start=1):
print(f"{number}. {route}")八、遞迴不一定是唯一答案
| 情境 | 較適合 | 原因 |
|---|---|---|
| 樹狀資料、資料夾、巢狀選單 | 遞迴 | 程式結構貼近資料結構 |
| 固定範圍的重複工作 | for/while | 直觀且沒有遞迴深度問題 |
| 層數可能非常深 | 自行維護 stack 的迴圈 | 避免超過 Python 遞迴深度 |
以下以串列當作堆疊,完成相同的深度優先走訪:
def print_tree_iterative(root):
stack = [(root, 0)]
while stack:
node, depth = stack.pop()
print(f"{' ' * depth}• {node['名稱']}")
for child in reversed(node["子項目"]):
stack.append((child, depth + 1))
print_tree_iterative(cultural_tree)九、真實資料要防範循環參照
純樹狀資料每個節點只會向下;若節點互相連結,就可能繞回已拜訪的位置。可以記錄物件識別值,並設定最大深度。
def safe_print(node, depth=0, max_depth=20, visited=None):
if visited is None:
visited = set()
if depth > max_depth:
print(" " * depth + "…已達深度上限")
return
node_id = id(node)
if node_id in visited:
print(" " * depth + "…偵測到重複參照")
return
visited.add(node_id)
print(" " * depth + "• " + node["名稱"])
for child in node["子項目"]:
safe_print(child, depth + 1, max_depth, visited)
safe_print(cultural_tree)十、常見錯誤除錯表
| 現象 | 可能原因 | 修正方式 |
|---|---|---|
| RecursionError | 沒有停止條件,或資料形成循環 | 加入基底條件、visited 與深度上限 |
| 一直處理同一筆資料 | 遞迴步驟沒有縮小問題 | 確認傳入的是子節點或較小參數 |
| 第二次搜尋出現舊路徑 | 預設參數寫成 path=[] | 改用 None,函式內再建立串列 |
| 明明找到卻得到 None | 遞迴結果沒有向上回傳 | 接住 result,找到時立刻 return |
| 資料很深時失敗 | 超過 Python 遞迴深度 | 改用自行維護 stack 的迴圈 |
十一、USR 實作挑戰
訪談後新增一個分類與三個子節點;為每筆資料加入「來源」與「是否可公開」欄位,列印時只顯示可公開項目。
加入「建議停留分鐘」欄位,以遞迴計算每個分類及全程所需時間,再找出時間最長的導覽分支。
把文化樹存成 JSON,讀取後執行搜尋。這會銜接後續的檔案處理與網頁資料章節。
十二、與生成式 AI 協作
可以請 AI 協助檢查程式,但地方知識仍需由社區確認。以下提示詞可直接修改:
請擔任 Python 助教。以下是一份水井村教學用樹狀資料與遞迴函式。
請依序檢查:
1. 是否有明確的基底條件;
2. 每次遞迴是否讓問題縮小;
3. 回傳值是否能逐層傳回;
4. 是否有循環參照或過深風險。
請只提出程式結構建議,不要自行補寫未經社區確認的地方史實。遞迴不是「函式一直叫自己」而已,而是:先定義停止點,再讓每一步都更接近停止點。面對地方文化分類、導覽目錄與其他巢狀資料時,它能讓程式像資料本身一樣清楚。
沒有留言:
張貼留言