【考試向】資料結構筆記(樹及二元樹)
【考試向】資料結構筆記(樹及二元樹)
歡迎你點入本篇文章!我是 LukeTseng,本系列文章主要整理自學資料結構的一些知識,如果你喜歡我的文章,麻煩您不吝嗇的在文章底下按下一顆愛心,或是追蹤我唷~
簡介(Introduction)

Image Source:https://zh.wikipedia.org/zh-tw/%E6%A0%91_(%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84)
樹(Tree)資料結構是一種非線性資料結構,用來模擬具有階層性(Hierarchical)關係的資料。
一棵樹的基本構成由以下兩個要素組成:
- 節點(Node):樹的基本組成單位,每個節點會包含資料(或鍵值),以及指向其子節點的指標。
- 邊(Edge / Link):連接兩節點的線條,代表節點之間的關係,一棵有 $N$ 個節點的樹,必定會有 $N-1$ 條邊。
節點關係的術語
- 根節點(Root):樹的最頂層節點,整棵樹只有一個根節點,且是唯一沒有父節點的節點。
- 如上圖中的節點 A 即根節點。
- 父節點(Parent):若一個節點向下連接到其他節點,該節點就是那些節點的父節點。
- 如上圖中的節點 A 為 B、C 這兩個節點的父節點。
- 子節點(Child):由父節點向下連接延伸出的節點。
- 如上圖中的節點 B、C 是節點 A 的子節點。
- 兄弟節點(Sibling):擁有同一個父節點的節點們互稱兄弟節點。
- 如上圖中的節點 B、C 擁有共同父節點 A,因此 B、C 兩節點是兄弟節點。
- 葉節點(Leaf / External Node):沒有任何子節點的終端節點(terminal node)(即位於樹的最底層分支末端)。
- 如上圖中的節點 K、O、P 都沒有子節點,因而稱為葉節點。
- 內部節點(Internal Node):除了葉節點以外的節點,也就是至少擁有一個子節點的節點。
- 祖先(Ancestor)節點與子孫(Descendant)節點:從根節點往下走到某特定節點的路徑上,經過的所有節點都是該節點的「祖先」;反之,某節點向下延伸出的所有節點都是它的「子孫」。
- 子樹(Subtree):樹中的任何一個節點,連同它所有的後代節點,可以被看作是一棵獨立的小樹,稱為子樹。
- 如上圖中的節點 C、G、L、M、O、H、N、P 可稱為一個子樹。
測量與屬性的術語
- 分支度(Degree):一個節點所擁有的子節點(或子樹)數量。而「整棵樹的分支度」是指該樹中所有節點分支度的最大值(例如二元樹的最大分支度為 2)。
- 如上圖中的節點 A 的分支度為 2,因為它有兩個子節點。
- 深度(Depth):從根節點走到某特定節點所需要經過的邊數。根節點的深度通常定義為 0。
- 如上圖中的節點 C 的深度為 1,因為從根節點到 C 只要經過一個邊;而節點 E 的深度為 2。
- 高度(Height):從某特定節點走到最遠的葉節點所經過的最長路徑邊數。葉節點的高度通常定義為 0,整棵樹的高度就是根節點的高度。
- 如上圖中的節點 A 的高度為 4(葉節點的高度是 0,往上爬依序是 0, 1, 2, 3, 4),即為整棵樹的高度。
- 階度(Level):代表節點所在的階層位置。通常將根節點定義為 Level 1(有些定義為 Level 0),它的子節點為 Level 2,以此類推。
樹林(Forest)
樹林(Forest)是由零棵或多棵互不相交(Disjoint)的樹所構成的集合。
若將一棵樹的根節點(Root)移除,且不改變其餘節點的連結關係,那其底下原本互相獨立的各個子樹(Subtrees),就會共同形成一個樹林。
反之,若建立一個新的單一節點,並將一個樹林中所有樹的根節點都連向這個新節點作為它的子節點,這個樹林就會變成一棵單一的樹。
樹的種類
- 二元樹(Binary Tree):每個節點最多只有兩個子節點(左子節點與右子節點),此為大多數複雜樹狀結構的基礎。
- 二元搜尋樹(Binary Search Tree, BST):一種具有順序性質的二元樹。規定左子樹所有節點的值必須「小於」父節點,右子樹所有節點的值必須「大於」父節點,適合用於實作動態集合或字典。
- 平衡樹(Balanced Trees):為了解決一般 BST 在極端情況下(如依序插入遞增數列)會退化成鏈結串列(導致搜尋變為 $O(n)$)的問題,常見的實作有 AVL Tree 和紅黑樹(Red-Black Tree),它們會透過旋轉機制自動保持樹的高度平衡。
- 堆積(Heap):一種特殊的完全二元樹,分為最大堆積(父節點恆大於子節點)和最小堆積。常用於實作優先佇列(Priority Queue)與堆積排序法(Heap Sort)。
- B 樹與 B+ 樹(B-Tree / B+ Tree):一種多路搜尋樹,節點可以擁有超過兩個以上的子節點。被廣泛應用在關聯式資料庫(Database Indexing)與檔案系統中,專為減少磁碟 I/O 讀取次數而設計。
- 字典樹 / 前綴樹(Trie):專門用來儲存與搜尋字串的樹狀結構。利用字串的共同前綴來節省空間,在搜尋引擎的自動完成(Auto-complete)或網路 IP 路由匹配中極為常見。
樹的資料結構表示法
以下是 $N$ 元樹的資料結構表示法:

Image Source:筆者繪製。
可用 C 語言撰寫成這樣:
1 |
|
樹在記憶體實作上的指標計算
以 C 語言實作樹狀結構時,每個節點通常會預先宣告固定數量的指標(有些書會稱之為 LINK),指向子節點。
此時會產生「實際使用的指標」與「浪費掉的空指標」的計算問題。
假設有棵分支度(Degree)為 $k$ 的 $k$ 元樹(k-ary tree),且樹中總共有 $N$ 個節點:
- 總指標數:每個節點都有 $k$ 個指標欄位,整棵樹總共有 $N \cdot k$ 個指標。
- 已使用的指標數:這些指標的作用是連接子節點,也就是樹的邊數,而每個節點均被一個指標所指向,所以已使用的指標數恆為 $N-1$。
最後求得空指標數(總指標數減去已使用的指標數):
因此會有 $Nk - N + 1$ 個指標(LINK)被浪費掉。
練習
設有一棵樹分支度為 6,且有 50 個節點,則該棵樹需要多少個指標(或稱 LINK)欄位,而實際上用了多少個,浪費多少個指標?
- 計算總共需要多少個指標欄位: $N \cdot k = 6 \cdot 50 = 300$
- 計算浪費多少個指標: $N(k - 1) + 1 = 50(6 - 1) + 1 = 251$
- 計算實際上用了多少個:
- $300 - 251 = 49$
- 或是 $N - 1 = 50 - 1 = 49$ (因為無論樹的分支度是多少,只要是一棵連通的單一樹,有 $N$ 個節點就必定只有 $N-1$ 條邊)
二元樹(Binary Tree)
二元樹是一種非線性且階層式(hierarchical)的資料結構。
二元樹是由節點(Node)組成的有限集合,該集合可為空(Empty),或者是由一個根節點(Root)以及兩棵互不相交的、分別稱為左子樹(Left Subtree)與右子樹(Right Subtree)的二元樹所組成。
限制:二元樹中的每一個節點,最多只能擁有兩個子節點(即分支度 Degree $\le 2$)。
下圖是一棵二元樹,可見每個節點僅有兩個子節點。

Image Source:https://www.geeksforgeeks.org/dsa/introduction-to-binary-tree/
二元樹與一般樹的不同
| 比較項目 | 二元樹(Binary Tree) | 一般樹 |
|---|---|---|
| 最大分支度 | 嚴格限制最多為 2。 | 沒有限制,可為 0 到無限大。 |
| 子樹的次序 | 具有排序順序的關係,嚴格區分左子樹與右子樹。即使某節點只有一個子節點,也必須明確指出它是左子節點還是右子節點。 | 不區分左右次序。如果只有一個子節點,它就是該節點唯一的子節點,沒有左右之分。 |
| 空樹的允許 | 可為完全沒有任何節點的空集合。 | 傳統定義上不能為空,至少必須包含一個節點(根節點)。 |
不同型態的二元樹
左斜樹(Left Skewed Tree)與右斜樹(Right Skewed Tree)
- 左斜樹:樹中的每一個非葉節點,都只有左子節點。
- 右斜樹:樹中的每一個非葉節點,都只有右子節點。
- 特性:
- 這兩種樹的形狀會完全退化成一條直線,本質上等同於單向鏈結串列(Singly Linked List)。
- 出現這兩種樹會導致樹的高度等於節點總數 $N$,使得搜尋資料的時間複雜度從對數時間 $O(\log n)$ 劣化為線性時間 $O(n)$。

Image Source:https://www.geeksforgeeks.org/dsa/types-of-binary-tree/
滿枝二元樹(Fully Binary Tree)
定義:一棵深度(或階度 level)為 $k$ 的二元樹,如果擁有剛好 $2^k - 1$ 個節點,這棵樹就是滿枝二元樹(假設根節點深度為 $1$ 的時候成立)。
特性:除了葉節點(leaf node)外,所有的內部節點(internal node)皆擁有 2 個子節點,不允許只有一個子節點。

Image Source:https://www.geeksforgeeks.org/dsa/what-is-full-binary-tree/
完整二元樹(Complete Binary Tree)
定義:一棵有 $n$ 個節點、深度為 $k$ 的二元樹,如果將它從上到下、從左至右依次編號(從 $1$ 到 $n$),其每個節點的位置,都能夠與同深度的「滿枝二元樹」中編號 $1$ 到 $n$ 的節點位置完全對應。
意思就是除了最底層之外,上面所有層級都被完全填滿;而在最底層的節點,必須嚴格遵守從左側開始連續填滿,中間不能有空隙的規則。
要辨別完整二元樹就是看他倒數第二層以上是否都有全部被填滿,如下圖中倒數第二層 Level 2 以上到 Level 0,全部都有兩個子節點,而 Level 3 中的 J 沒有兄弟節點。

Image Source:https://www.geeksforgeeks.org/dsa/difference-between-full-and-complete-binary-tree/
應用:完整二元樹適合用一維陣列來儲存(完全不浪費陣列空間),為實作堆積(Heap)與優先佇列(Priority Queue)的基礎。
二元樹的數學性質
- 第 $i$ 階度(Level)的最大節點數:在二元樹的第 $i$ 層中,最多有 $2^{i-1}$ 個節點(假設根節點在第 1 層, $i \ge 1$)。
- 階度 $k$ 的最大節點數:一棵階度為 $k$ 的二元樹,最多有 $2^k - 1$ 個節點(假設根節點在第 1 層, $i \ge 1$)。
- 葉節點與分支度(Degree)為 $2$ 的節點關係:對於任何一棵非空的二元樹,如果其葉節點(分支度為 $0$)的數量為 $N_0$,且分支度為 $2$ 的節點數量為 $N_2$,則必然存在以下公式:
- 葉節點的數量永遠比擁有兩個子節點的內部節點數量多出 1 個。
如下圖中階度 $3$ 的最大節點數為 $2^3 = 8$ 個節點(因為根節點階度是 $0$,不用 $- 1$),而全部最大節點數為 $2^4 - 1 = 15$ ($0 \sim 3$ 階度總共有四個階度)。

Image Source:https://www.geeksforgeeks.org/dsa/difference-between-full-and-complete-binary-tree/
完整二元樹的數學性質
完整二元樹最著名的特性,即父、子節點之間的關係可透過簡單的代數運算來定位,使遍歷與節點交換的時間複雜度極低。
若根節點儲存在陣列索引 $1$ 的位置(1-based indexing):
對於陣列中的任意節點 $i$($1 \leq i \leq n$):
- 父節點:
- 若 $i = 1$ 則為根節點,無父節點。
- 若 $i \ne 1$,則第 $i$ 個節點的父節點位於索引 $\lfloor \frac{i}{2} \rfloor$ ($\lfloor \frac{i}{2} \rfloor$ 表示 $< \frac{i}{2}$ 的最大正整數,即為 floor 函數)。
- 左子節點:
- 若 $2i \le n$ ,則節點 $i$ 的左子節點(left child)索引在 $2i$。
- 若 $2i > n$,代表該節點為葉節點,無左子節點。
- 右子節點:
- 若 $2i + 1 \le n$ ,則節點 $i$ 的右子節點(right child)索引在 $2i + 1$ 。
- 若 $2i + 1 > n$,代表無右子節點。
若根節點儲存在陣列索引 $0$ 的位置(0-based indexing):
- 父節點:$\lfloor \frac{i - 1}{2} \rfloor$。
- 左子節點:$2i + 1$。
- 右子節點:$2i + 2$。
以上的這些公式用在「由上到下」、「由左至右」的方式編號的完整二元樹,如下圖:

Image Source:https://interviewing.io/questions/count-complete-tree-nodes
一維陣列表示二元樹
假設有棵樹長這樣(A、B、C、D、E、F 為資料,而括號內為編號、索引):

Image Source:筆者繪製。
該棵樹的最大節點數為 $2^3 - 1 = 7$ ,因此可將其表示成下列的一維陣列:

由於該棵樹是一棵完整二元樹(Complete Binary Tree),因此可用前面的公式來對該一維陣列做驗證:
- 驗證 B 節點($i=2$):
- 左子節點 $= 2 \times 2 = 4$(資料為 D)
- 右子節點 $= 2 \times 2 + 1 = 5$(資料為 E)
- 驗證 F 節點($i=6$):
- 父節點 $= 6 / 2 = 3$(資料為 C)
優缺分析
使用一維陣列表示法的優缺點:
- 優點:
- 節省空間(針對完整二元樹或滿枝二元樹):不需要儲存左右子樹的指標(Pointer),在 64 位元系統中可節省大量記憶體。
- 存取快速:利用索引計算可以直接跳轉到目標節點,時間複雜度為 $O(1)$。
- 適合堆積(Heap):該表示法是實作優先隊列(Priority Queue)或堆積排序(Heap Sort)的標準做法。
- 缺點:
- 空間浪費(針對稀疏樹):如果二元樹非常不平衡(例如右斜樹),中間會有大量空缺,陣列必須保留這些空間。
- 例如:深度為 $k$ 的右斜樹只有 $k$ 個節點,但陣列需要 $2^k - 1$ 的空間。
- 補充:完整二元樹也可能造成空間浪費,如上陣列圖就多出一個空間。
- 大小固定:陣列一旦宣告後較難動態擴張,若樹的深度增加,可能需要重新分配整個陣列。
- 插入與刪除節點效率低:新增或刪除節點時,可能需要大量搬移陣列中的資料來維持索引結構。
- 空間浪費(針對稀疏樹):如果二元樹非常不平衡(例如右斜樹),中間會有大量空缺,陣列必須保留這些空間。
鏈結方式表示二元樹
鏈結(Linked)表示法是實作二元樹最常見且最具彈性的方式。
不同於一維陣列表示法在處理「非完整二元樹、滿枝二元樹」時容易造成大量的記憶體空間浪費,鏈結表示法採用動態記憶體配置,反映樹狀結構的階層與分支關係。
另外鏈結方式也解決一維陣列插入與刪除效率低的問題。
在鏈結表示法中,二元樹是由一個個獨立的節點(Node)所串聯而成,每個節點通常包含三個基本區塊:
- 資料欄位(data):儲存該節點所要記錄的數值或資訊。
- 左子指標(Left Pointer, 下圖的 L_link):指向該節點的左子節點位置,如果沒有左子節點,此指標會設為空值(C 的
NULL或 C++nullptr)。 - 右子指標(Right Pointer, 下圖的 R_link):指向該節點的右子節點位置,同樣地,若無右子節點則設為空值。
整棵樹會由一個稱為根節點(Root)的指標作為起點,若這棵樹是空的,則 Root 指標本身就是空值。

Image Source:筆者繪製。
若寫成 C 語言,則會像下面這樣(不過通常都會寫成 llink 跟 rlink):
1 | struct TreeNode { |
鏈結樹的圖形示意:

Image Source:https://afteracademy.com/article/what-is-a-tree-data-structure
優缺分析
使用鏈結表示法的優缺:
- 優點:
- 動態記憶體:空間隨用隨取,只有在新增節點時才消耗記憶體,不像陣列需預先宣告一大塊連續空間而造成浪費。
- 插入刪除操作靈活:只需改變父節點與子節點之間的指標方向即可,不需像陣列大幅度搬移後續的資料。
- 缺點:
- 額外的空間開銷:每個節點除了儲存資料本身,都必須額外花費記憶體空間來儲存兩個指標(左、右)。
- 無法隨機存取:不能像陣列一樣透過索引值(Index)在 $O(1)$ 的時間內直接找到特定節點,必須從根節點(Root)開始,循著指標一步一步往下遍歷尋找。
二元樹的追蹤(traversal)
二元樹的追蹤,或稱遍歷,有以下三種:
- 前序遍歷(Preorder Traversal):先處理當前節點,再處理子節點。這種方式常用於複製整棵二元樹,或是輸出結構化的文件(如前綴表示法)。
- 拜訪順序:根節點(Root) → 左子樹(Left Subtree) → 右子樹(Right Subtree)
- 中序遍歷(Inorder Traversal):先處理左邊,再處理中間,最後處理右邊。對於二元搜尋樹(Binary Search Tree, BST)來說,中序遍歷的結果會是一個由小到大排序好的數列。
- 拜訪順序:左子樹(Left Subtree) → 根節點(Root) → 右子樹(Right Subtree)
- 後序遍歷(Postorder Traversal):先處理完所有子節點,最後才處理當前節點。這種方式常用於刪除整棵二元樹,因為必須要先清空左、右子節點的記憶體,最後才能安全釋放父節點。
- 拜訪順序:左子樹(Left Subtree) → 右子樹(Right Subtree) → 根節點(Root)
繼續以下 C 語言實作前,要先定義二元樹結構:
1 |
|
C 語言實作:前序遍歷
1 | void preorderTraversal(struct Node* node) { |
在主程式執行:
1 | int main(){ |
Output:
1 | 10 20 30 |
C 語言實作:中序遍歷
1 | void inorderTraversal(struct Node* node) { |
執行結果:
1 | 20 10 30 |
C 語言實作:後序遍歷
1 | void postorderTraversal(struct Node* node) { |
執行結果:
1 | 20 30 10 |
三種遍歷方式比較表
| 遍歷名稱 | 英文名稱 | 執行順序 | 常見應用 |
|---|---|---|---|
| 前序遍歷 | Preorder | 根 → 左 → 右 | 複製二元樹、建立目錄結構 |
| 中序遍歷 | Inorder | 左 → 根 → 右 | 二元搜尋樹(BST)排序輸出 |
| 後序遍歷 | Postorder | 左 → 右 → 根 | 安全地釋放(刪除)二元樹的記憶體 |
以非遞迴形式實作的前、中、後序遍歷
所謂非遞迴形式就是用堆疊(Stack)的方式實現。
首先準備一個簡單的堆疊結構:
1 |
|
前序遍歷
前序遍歷的順序是中 -> 左 -> 右。
由於堆疊為後進先出(LIFO)的資料結構,所以當處理完中間節點後,必須先將右子節點推入堆疊,再推入左子節點,這樣下次取出的時候,才會先取出左子節點。
1 | void preorderTraversal(struct Node* root) { |
執行以下主程式:
1 | int main() { |
Output:
1 | 10 20 30 |
中序遍歷
中序遍歷的順序是左 -> 中 -> 右。
需要一個指標 curr 來持續往左走,沿途將經過的節點都推入堆疊,當走到盡頭(左子樹為空)時,從堆疊取出一個節點印出,接著轉向該節點的右子樹,重複同樣的過程。
1 | void inorderTraversal(struct Node* root) { |
執行結果:
1 | 20 10 30 |
後序遍歷
後序遍歷的順序是左 -> 右 -> 中。
後序遍歷在非遞迴處理上是最複雜的:當從左子樹返回中間節點時,還不能馬上印出中間節點,必須先去處理右子樹。
在此使用單一堆疊搭配一個 lastVisited 指標的方法:用以記錄上一個印出的節點。
若當前節點的右子樹為空,或者右子樹剛已被印出過(lastVisited == curr->right),就代表左右子樹都處理完了,可直接印出當前節點。
1 | void postorderTraversal(struct Node* root) { |
二元樹的性質
一般樹轉二元樹
將一般樹轉換為二元樹最標準且常用的方法稱為左子右兄弟(Left-Child Right-Sibling)表示法。
該方法可讓任何一棵一般樹(甚至是一座森林)轉換成一棵二元樹。
轉換上共有三個步驟:
- 兄弟相連:將所有同層級、同一個父節點的兄弟節點,從左至右用橫線連接起來。
- 保留長子、砍掉剩下的:對每個父節點,除了保留與第一個子節點(最左邊的子節點)的連線外,刪除與其他所有子節點的垂直連線。
- 順時針旋轉 45 度:將整棵樹向右(順時針)傾斜 45 度,重新整理層次,原本往下的垂直線變成二元樹的左子樹,原本橫向的兄弟連線變成二元樹的右子樹。
所謂順時針轉 45 度,就像下圖中那樣:

Image Source:筆者繪製。
那如果是橫的呢?請看下圖:

而所謂兄弟相連、保留長子就是像下面那樣:

B、C、D 互相都是兄弟節點,而長子就是最左邊的節點 B。
透過前、中、後序遍歷決定唯一的二元樹
如何透過走訪序列還原一棵唯一的二元樹?必須要有中序遍歷(In-order),再搭配前序遍歷(Pre-order)或後序遍歷(Post-order)其中之一。
如果只有前序和後序,是無法決定一棵唯一的二元樹的。
為何單一遍歷無法決定結構?
無論是前、中、後序,本質上都是將 2D 的樹狀結構降維壓扁成 1D 的線性陣列,在此過程中,父子節點的邊界資訊遺失了。
只知道節點遍歷的先後順序,卻不知道誰是誰的左子樹或右子樹。
定位與切割
要精準還原二元樹的結構,需要兩種不同的資訊交互配合:
- 前序或後序,負責找根節點(Root):
- 前序(中 -> 左 -> 右):序列的第一個元素,絕對是當前樹的根節點。
- 後序(左 -> 右 -> 中):序列的最後一個元素,絕對是當前樹的根節點。
- 中序負責切割左右子樹:
- 中序(左 -> 中 -> 右):一旦透過前序或後序知道了「根節點」是誰,回到中序序列中找到該節點的位置,就可將序列切成兩半,即左邊的全是左子樹節點,右邊的全是右子樹節點。
前序 + 中序範例
假設題目提供兩組序列,求出完整唯一的二元樹:
- 前序(Pre-order):
[A, B, D, E, C, F] - 中序(In-order):
[D, B, E, A, F, C]
第一輪(做確認與切割):
- 看前序的第一個元素,確認根節點是
A。 - 拿著
A去中序找,發現中序被切成兩半:[D, B, E](左子樹)和[F, C](右子樹)。
第二輪(處理左子樹):
- 回到前序,跳過
A之後往後數3個節點[B, D, E],即左子樹的前序序列。 - 左子樹的前序是
[B, D, E],第一個元素是B,所以左子樹的根節點是B。 - 拿
B去中序的[D, B, E]找,切出:[D](左子樹)和[E](右子樹)。
第三輪(處理右子樹):
- 回到前序,跳過
A之後找出[C, F],即右子樹的前序序列。 - 右子樹的前序是
[C, F],第一個元素是C,所以右子樹的根節點是C。 - 拿
C去中序的[F, C]找,切出:[F](左子樹)。
最後求出二元樹:

後序 + 中序範例
假設題目給了兩組序列:
- 後序(Post-order):
[D, E, B, F, C, A] - 中序(In-order):
[D, B, E, A, F, C]
第一輪(做確認與切割):
- 看後序的最後一個元素,
A即為根節點。 - 拿
A去中序找,將中序序列分兩半:[D, B, E](左子樹)、[F, C](右子樹)。
第二輪(處理左子樹):
- 看左子樹後序
[D, E, B]的最後一個元素是B,所以B是這棵左子樹的根節點(A的左子節點)。 - 拿著
B去左子樹中序[D, B, E]中尋找,可切成[D](左子樹)、[E](右子樹)。
第三輪(處理右子樹):
- 看右子樹後序
[F, C]的最後一個元素是C,所以C是這棵右子樹的根節點(A的右子節點)。 - 拿著
C去右子樹中序[F, C]中尋找,可切成[F](左子樹)。
最後得出的結果與前序 + 中序的結果相同(此為範例刻意設計,後續遇到不同題目並不代表前序算出的結果跟後序一樣),一樣是這張圖:

引線二元樹(Thread Binary Tree)
引線二元樹(Thread Binary Tree)又稱線索二元樹,為傳統二元樹的一種改良結構。
概念是利用二元樹中原本空著的(Null)指標,來記錄節點在某種遍歷順序下的前行者(Predecessor)和後繼者(Successor)節點。
:::info
- 前行者 (Predecessor)
- 定義:
在某種遍歷順序下,緊接在當前節點「之前」被拜訪的那個節點。
若把遍歷的結果攤平寫成一排直線的陣列,前行者即當前節點左邊的那一個元素。 - 如何在樹的結構中找到中序前行者?
假設找節點 $X$ 的中序前行者:- 情況 A(有左子樹):如果 $X$ 有左子樹,則其前行者就是其左子樹中最右下角的節點,相當於在左子樹中尋找數值最大的節點。
- 情況 B(沒有左子樹):如果 $X$ 沒有左子樹,必須沿著父節點往上找,找到第一個「把它當作右子孫」的祖先節點,則祖先節點就是 $X$ 的前行者。
- 定義:
- 後繼者(Successor)
- 定義:
在某種遍歷順序下,緊接在當前節點「之後」被拜訪的那個節點。
若把遍歷的結果攤平,後繼者就是當前節點右邊的那一個元素。 - 如何在樹的結構中找到中序後繼者?
假設我們要找節點 $X$ 的中序後繼者:- 情況 A(有右子樹):如果 $X$ 有右子樹,則其後繼者就是其右子樹中最左下角的節點,相當於在右子樹中尋找數值最小的節點。
- 情況 B(沒有右子樹):如果 $X$ 沒有右子樹,必須沿著父節點往上找,找到第一個「把它當作左子孫」的祖先節點,則祖先節點就是 $X$ 的後繼者。
:::
- 定義:
為什麼需要引線二元樹?
在一個有 $n$ 個節點的標準二元樹中,總共會有 $2n$ 個指標(每個節點有左、右兩個指標)。
但其中只有 $n-1$ 個指標真正指向了子節點(也就是只有 $n - 1$ 的指標被利用),剩下的 $n+1$ 個指標都是空的(Null)。
傳統上,如果要遍歷(例如中序遍歷)一棵二元樹,通常需要使用遞迴或堆疊來記錄回溯的路徑,會消耗額外的記憶體空間與時間。
引線二元樹就是為了解決該問題,將 $n+1$ 個浪費掉的空指標利用起來,將其變成引線(Threads),直接指向遍歷時的上一個或下一個節點,這樣一來,遍歷樹就不再需要遞迴或堆疊了。
引線(Thread)與標籤(Tag)
為區分一個指標到底是指向真正的子節點,還是指向前行者/後繼者節點的引線,需要在節點結構中加入兩個布林值標籤(Tags):
- 左指標(Left Pointer):
- 若有左子節點,指向左子節點。
- 若無左子節點,就將其變成左引線(Left Thread),指向遍歷順序中的前行者節點。
- 右指標(Right Pointer):
- 若有右子節點,指向右子節點。
- 若無右子節點,就將其變成右引線(Right Thread),指向遍歷順序中的後繼者節點。
- 標籤(Left Tag & Right Tag):
lTag = 1:左指標指向左子節點。lTag = 0:左指標是引線,指向前行者節點。rTag = 1:右指標指向右子節點。rTag = 0:右指標是引線,指向後繼者節點。
在 C 中,這樣的資料結構可定義成:
1 | // 使用 bool 請引入 <stdbool.h> |
圖解:

Image Source:筆者繪製。
引線二元樹的長相
下圖實線代表一般正常的子節點指標(Link),虛線代表引線(Thread),也就是那些被重新利用的空指標。

Image Source:https://www.geeksforgeeks.org/dsa/threaded-binary-tree/
如何將二元樹轉引線二元樹
以最常見的中序引線二元樹為例。
不需改變原本樹的形狀或實體節點的鏈結,只需把那些原本指向空(Null)的指標,依照中序遍歷(左 -> 中 -> 右)的順序,重新指向它們的前行者或後繼者節點。
要做到二元樹轉引線二元樹,則要在遍歷整棵樹的過程中,必須隨時記住剛剛拜訪過的那一個節點。
有兩個變數:
Curr(現在的節點)Prev(上一個節點)
現在開始做中序遍歷(初始化 Curr = 最左邊的節點, Prev = NULL):
- 處理當前節點(
Curr)的左引線- 若左指標為空,代表無實體的左子節點。
- 此時把
Curr的左指標指向Prev。 - 並把
Curr的左標籤(lTag)標記為引線,看你定義 1 or 0 是引線就是哪一個。
- 處理上一個節點的右引線
- 如果
Prev的右指標為空,代表無實體的右子節點。 - 因為現在位於
Curr,而Curr剛好就是Prev的下一步,所以,將Prev的右指標指向目前的Curr。 - 並把
Prev的右標籤(rTag)標記為引線。
- 如果
- 前進
- 當
Curr的左指標和Prev的右指標都檢查且牽好線之後,就讓Prev移動到Curr的位置(現在的節點即將變成下一次檢查時的上一個節點),然後繼續中序遍歷的下一步。
- 當
- 處理最後一個節點
- 當整棵樹都遍歷完畢後,
Prev會停留在整棵樹中序遍歷的最後一個節點(整棵樹最右下角的節點)。 - 該節點的右指標必定為空。
- 因為是最後一個,沒有下一個節點可指向,所以其右指標會保持為空(Null),並將右標籤標記為引線。
- 當整棵樹都遍歷完畢後,
範例
假設有棵樹長這樣:

Image Source:筆者繪製。
則其中序遍歷會是 1 -> 2 -> 3 -> 4 -> 6
設 Curr = 1,Prev = NULL,因為最左邊那個節點沒有前一個節點。
Curr(1)的左指標為空,因此把節點 1 的左引線指向 Prev(Null)。
接著再看 Prev 的右指標,而由於 Prev 現在是 NULL,所以啥事都不用幹,然後現在圖會變這樣:

前進走到節點 2(Curr = 2, Prev = 1):
- 檢查
Curr(2)的左指標:有指向實體的節點 1,非空,所以不管他。 - 檢查
Prev(1)的右指標:為空,代表節點 1 需要一條後繼者引線,把節點 1 的右引線指向Curr(2)。

接著前進走到節點 3(Curr = 3, Prev = 2):
- 檢查
Curr(3)的左指標:為空,將節點 3 的左引線指向Prev(2)。 - 檢查
Prev(2)的右指標:有指向實體的節點3,非空,所以不管他。

然後走到節點 4(Curr = 4, Prev = 3):
- 檢查
Curr(4)的左指標:有指向實體節點 2,不管他。 - 檢查
Prev(3)的右指標:為空,將節點 3 的右引線指向Curr(4)。

最後走到節點 6(Curr = 6, Prev = 4):
- 檢查
Curr(6)的左指標:為空,將節點 6 的左引線牽向Prev(4)。 - 檢查
Prev(4)的右指標:有指向實體節點 6,不管他。

現在所有節點都走完了,做最後的收尾,Prev 停在最後一個節點 6,此時看他的右指標,會是空的,但他後面沒人了,所以將其右引線保持為 Null,並標記為引線。

C 語言實作:尋找中序後繼者(Successor)
資料結構定義:
1 |
|
接著實作尋找中序後繼者,如同前面所說的,會有兩種情形:
- 情況 A:若其右指標是引線(
rTag == 0),則該條引線即指向後繼者的,直接回傳rlink即可。 - 情況 B:若其右指標是右子樹(
rTag == 1),則其後繼者,即右子樹裡面最左邊的節點。
1 | ThreadNode* inorderSuccessor(ThreadNode *curr) { |
C 語言實作:尋找中序前行者(Predecessor)
- 情況 A:若左指標是引線(
lTag == 0),直接回傳llink。 - 情況 B:若左指標是左子樹(
lTag == 1),則前行者即左子樹裡面最右邊的節點。
1 | ThreadNode* inorderPredecessor(ThreadNode *curr) { |
C 語言實作:引線二元樹的中序遍歷
有了尋找後繼者的函式後,遍歷整棵樹就會變得簡單,從此不再需要遞迴或堆疊,只要:
- 找出整棵樹的起點(由於中序遍歷,因此在整棵樹最左邊的節點)。
- 用
while迴圈,不斷呼叫inorderSuccessor往後跳,直到指標變成NULL為止。
1 | void inorderTraversal(ThreadNode *root) { |
C 語言程式實作:引線二元樹的插入操作(插入右方)
假設要將 newNode 插入到 curr 節點的右方,則插入的兩種情況:
- 情況一:
curr原本沒有右子樹(curr->rTag == 0)- 最簡單的狀況,
newNode直接變成curr的右子節點。newNode的左引線指回curr(curr為他的前行者)newNode的右引線會接上curr原本的右引線(指向curr原本的後繼者)。
- 最簡單的狀況,
- 情況二:
curr原本已經有右子樹(curr->rTag == 1)- 此時
newNode會擠進curr和它原本的右子樹之間。newNode成為curr的新右子節點。curr原本的右子樹,變成newNode的右子樹。- 原本右子樹裡最左下角的節點,其左引線原本是指向
curr,現在中間多了newNode,所以必須把那條左引線更新,改為指向newNode。
- 此時
程式實作:
1 | void insert(ThreadNode *curr, ThreadNode *newNode) { |
優缺分析
以下是引線二元樹的優缺分析:
- 優點:
- 節省空間:不需額外的堆疊就能進行遍歷。
- 提升遍歷效率:遍歷的過程變成了單純的指標跳躍,速度比傳統遞迴的二元樹更快。
- 快速尋找鄰居:可非常快速找到任意節點在特定遍歷下的前行者或後繼者節點。
- 缺點:
- 插入與刪除複雜:每當樹的結構發生改變(新增或刪除節點)時,除了要修改子節點指標,還必須維護引線的正確性,邏輯較為繁瑣。
- 節點變大:每個節點要多儲存兩個標籤(
lTag,rTag),雖然布林值佔用空間不大,但依然增加了單一節點的大小。
總整理
樹(Tree)基本概念
樹是一種非線性、階層式的資料結構。
若有一棵 $N$ 個節點的連通樹,必定恰好有 $N-1$ 條邊。
重要術語:
- 根節點(Root):最頂層、唯一沒有父節點的節點。
- 葉節點(Leaf/Terminal Node):沒有任何子節點的末端節點。
- 內部節點(Internal Node):至少有一個子節點的節點。
- 分支度(Degree):節點擁有的子節點或子樹數量。整棵樹的分支度為最大節點分支度。
- 深度(Depth):從根節點往下走到目標節點的邊數(根為 0 或 1,依定義而定)。
- 高度(Height):從目標節點往上算到最遠葉節點的邊數(葉為 0)。
- 樹林(Forest):移除樹的根節點,底下的子樹集合即為樹林;反之,將多棵樹連上一個共同的根,就變成一棵樹。
考點:$k$ 元樹的空指標計算
- 實作 $k$ 元樹時,每個節點配置 $k$ 個指標,若樹有 $N$ 個節點:
- 總指標數:$N \cdot k$
- 已使用指標(邊數):$N - 1$
- 浪費掉的空指標:$N(k - 1) + 1$
二元樹(Binary Tree)
二元樹的最大分支度嚴格限制為 2,並且嚴格區分左、右子樹(即使只有一個子節點也要分左右)。
二元樹允許為空集合。
- 特殊型態的二元樹
- 斜樹(Skewed Tree):全偏向左或右,退化成鏈結串列,搜尋時間從對數退化為 $O(n)$。
- 滿枝二元樹(Full Binary Tree):每一層都長滿。深度 $k$ 的樹必有 $2^k - 1$ 個節點。
- 完整二元樹(Complete Binary Tree):除了最底層外全滿,且最底層嚴格由左至右連續填滿。適合用陣列實作(如 Heap)。
- 二元樹的數學性質
- 第 $i$ 階度的最大節點數:$2^{i-1}$
- 階度 $k$ 的整棵樹最大節點數:$2^k - 1$
- 葉節點與度數 $2$ 節點的關係:(葉節點永遠比有兩個子節點的內部節點多 $1$ 個)
- 儲存表示法
| 表示法 | 特性與優缺點 | 節點索引關係(以 1-based index 為例) |
|---|---|---|
| 一維陣列 | 優點:適合完整/滿枝二元樹,存取快 $O(1)$。 缺點:若樹不平衡會嚴重浪費空間。 | 父節點:$\lfloor i/2 \rfloor$ 左子節點:$2i$ 右子節點:$2i+1$ |
| 鏈結表示法 | 優點:最常用、動態配置記憶體,插入/刪除快。 缺點:需額外空間存指標,無法隨機存取。 | 節點包含:data, llink, rlink |
二元樹的追蹤(Traversal)
將 2D 樹狀結構攤平成 1D 序列的過程,分為遞迴與非遞迴(需依賴堆疊 Stack)實作。
| 遍歷方式 | 拜訪順序 | 常見應用 |
|---|---|---|
| 前序(Preorder) | 中 → 左 → 右 | 複製整棵樹、建立目錄結構 |
| 中序(Inorder) | 左 → 中 → 右 | BST 的排序輸出(由小到大) |
| 後序(Postorder) | 左 → 右 → 中 | 刪除整棵樹 |
樹的轉換與結構還原
- 一般樹 $\rightarrow$ 二元樹:左子右兄弟法
- 利用 Left-Child Right-Sibling 技巧:
- 兄弟相連:同層兄弟水平連線。
- 保留長子:父節點只保留與最左側子節點的垂直連線。
- 順時針轉 45 度:原本垂直變左子樹,原本水平連線變右子樹。
- 利用 Left-Child Right-Sibling 技巧:
- 透過遍歷序列還原唯一的二元樹
- 必備條件:必須有中序,搭配前序或後序才能還原。
- 前序 / 後序的作用:負責找出根節點(Root)。(前序的第一個,或後序的最後一個)。
- 中序的作用:拿著找出的根節點,回到中序序列中,將其切割出左子樹與右子樹的範圍,反覆遞迴即可畫出整棵樹。
- 必備條件:必須有中序,搭配前序或後序才能還原。
引線二元樹(Thread Binary Tree)
引線二元樹是為了解決傳統二元樹留下 $N+1$ 個空指標的浪費,並免除追蹤時對 Stack/遞迴的依賴。
- 概念:將空的左/右指標,改為指向中序遍歷下的前行者(Predecessor)或後繼者(Successor)。
- 標籤(Tag):為區分是指向實體子樹還是引線(Thread),節點需加入
lTag與rTag(布林值)。Tag = 1:指標連向實體子節點。Tag = 0:指標作為引線,連向鄰居。
如何尋找中序鄰居?
- 找後繼者(Successor):
- 若
rTag == 0:右指標就是後繼者。 - 若
rTag == 1:右子樹中最左下角的節點。
- 若
- 找前行者(Predecessor):
- 若
lTag == 0:左指標就是前行者。 - 若
lTag == 1:左子樹中最右下角的節點。
- 若


