演算法圖鑑
Tree · 02 / 07

Traversal前中後序與層序走訪

遞迴與迭代兩種寫法,層序用佇列

用在:算資料夾大小、序列化樹、運算式求值

時間複雜度O(n)
空間複雜度O(h)
難度入門
前置知識Binary Tree Basics、Queue、Stack

01為什麼需要它

計算資料夾大小要用後序

要知道一個資料夾多大,得先知道每個子資料夾多大。也就是「子節點先處理完,自己才能處理」。

為什麼用它後序走訪正是這個順序。刪除整棵樹、算子樹總和、判斷子樹是否平衡,凡是「答案由子樹組合而來」的都是後序。

序列化與複製一棵樹要用前序

把樹存成字串、傳到另一台機器、再還原成同樣的樹。或是複製一棵樹。

為什麼用它前序先記錄自己再往下,讀回來時第一個值就是根,能一邊讀一邊建。配合空節點的標記,一個前序序列就能唯一還原整棵樹。

BST 的中序就是排序結果

二元搜尋樹裡「左 < 自己 < 右」,要把所有值由小到大列出來。

為什麼用它先左、再自己、後右,正好是由小到大。驗證一棵樹是不是 BST、找第 k 小、找兩個錯位的節點,都靠中序。

一層一層印出組織圖要用層序

公司組織圖要按階層印:先所有副總、再所有經理。或者找離根最近的某個節點。

為什麼用它層序用佇列而不是遞迴,同一層的節點一定在下一層之前被處理,它就是樹上的 BFS。

看到這些關鍵字就想到它:先處理子節點還是自己、由小到大列出、按層印、序列化、樹上的 BFS 或 DFS。

02核心概念

走訪就是把每個節點恰好拜訪一次,差別只在順序。深度優先的三種走訪,遞迴骨架完全一樣:處理左子樹、處理右子樹、處理自己,差別是「處理自己」那一行放在前面(前序)、中間(中序)還是後面(後序)。名字裡的前中後,指的就是自己相對於左右子樹的位置。

怎麼選:答案要從上往下傳(例如路徑、深度)用前序;要從下往上組合(例如高度、子樹和)用後序;要有序輸出 BST 用中序。三種走訪都是 O(n) 時間、O(h) 空間,h 是樹高,那是遞迴呼叫堆疊的深度。

層序不用遞迴,用佇列:取出一個節點,把它的子節點放到尾端。因為佇列先進先出,同一層的節點一定排在下一層前面。若要「一層一層分組」,每一圈先記下目前佇列長度,只處理那麼多個。這就是圖論裡 BFS 的樹版本,空間是最寬那一層的節點數。

遞迴太深會爆堆疊時,改用明確的堆疊模擬。中序的迭代版最常考:一路往左把節點推入,走到底彈出處理,再轉向右子樹。理解它的關鍵是:堆疊裡放的是「左邊還沒處理完、自己也還沒處理」的節點。

03演算法步驟

  1. 1決定順序:答案往下傳用前序,答案往上組用後序,BST 要排序用中序,按層處理用層序
  2. 2遞迴版:if node is None: return,然後把「處理自己」放在遞迴左、遞迴右的前、中或後。
  3. 3層序版:佇列放入根;迴圈取出一個、處理、把子節點放入尾端。要分層就在每圈開頭記下佇列長度。
  4. 4需要迭代版時,用堆疊模擬:前序最簡單(先推右再推左);中序用「一路往左」的寫法;後序可以做「自右左」的前序再反轉。
  5. 5檢查複雜度:每個節點進出一次,O(n);額外空間是樹高(DFS)或最寬一層(BFS)。

04互動示範

切換四種走訪,逐步看拜訪順序。深度優先的三種顯示呼叫堆疊,層序顯示佇列。這棵樹的中序剛好是 1 到 7,因為它是一棵 BST。

1234567
呼叫堆疊(底 → 頂)
走訪順序
尚未開始
步驟 0/20中序走訪是遞迴:先走左子樹,再拜訪自己,最後走右子樹。

05程式碼

前中後序三個函式並排看,只有一行位置不同。層序用佇列並分層輸出,最後是中序的迭代版。

from collections import deque


# 三種深度優先走訪,只差「拜訪自己」放在哪一行
def preorder(node, out):
    if node is None:
        return
    out.append(node.val)          # 自己
    preorder(node.left, out)      # 左
    preorder(node.right, out)     # 右

def inorder(node, out):
    if node is None:
        return
    inorder(node.left, out)       # 左
    out.append(node.val)          # 自己
    inorder(node.right, out)      # 右

def postorder(node, out):
    if node is None:
        return
    postorder(node.left, out)     # 左
    postorder(node.right, out)    # 右
    out.append(node.val)          # 自己


# 層序:用佇列,一次處理一整層
def level_order(root):
    if root is None:
        return []
    out = []
    queue = deque([root])
    while queue:
        level = []
        for _ in range(len(queue)):   # 這一圈只處理目前這一層
            node = queue.popleft()
            level.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        out.append(level)
    return out


# 中序的迭代版:用明確的堆疊取代遞迴
def inorder_iter(root):
    out, stack, node = [], [], root
    while node or stack:
        while node:                   # 一路往左,沿途推入
            stack.append(node)
            node = node.left
        node = stack.pop()            # 沒有左邊了,處理自己
        out.append(node.val)
        node = node.right             # 換右子樹

06練習題

  • LeetCode 94Binary Tree Inorder Traversal(遞迴與迭代各寫一次)Easy
  • LeetCode 102Binary Tree Level Order TraversalMedium
  • LeetCode 199Binary Tree Right Side View(層序取每層最後一個)Medium
  • LeetCode 105Construct Binary Tree from Preorder and InorderMedium
  • LeetCode 297Serialize and Deserialize Binary TreeHard
  • LeetCode 236Lowest Common Ancestor(後序思維)Medium