---
title: 'Tree 樹狀結構'
disqus: kyleAlien
---
Tree 樹狀結構
===
## OverView of Content
如有引用參考請詳註出處,感謝 :smile:
**樹其實是圖 Graph 的一個子類,差異有**
1. 樹是無向的 (沒有方向)
2. 樹的分支是無法連通的
3. 樹沒有循環,末梢節點無法連接到根結點
> **樹狀結構**是==非線性==的資料結構,族譜、籃球比賽、組織圖、平面繪圖應用等等,可有二元、四元、八元樹
> ![](https://i.imgur.com/jCybbZk.png)
[TOC]
## 基本觀念
1. 有`根` (Root) 有`節點` (Node)
2. 節點之間互相連接,但**不可以形成無窮迴圈**
3. 去除根就會變成`樹林`(Forest),並且與其他樹是`互斥`的
> ![](https://i.imgur.com/52tfyk7.png)
### 樹專有名詞
1. 分支度 (degree): 節點的子樹個數
2. 階度 (level) : 樹的層級
3. 高度 (height) : 最高 level
4. 終端節點 (terminal) : 末梢無分支
> ![](https://i.imgur.com/56DMBp6.png)
## 二元樹
* 在電腦中儲存樹的方式是以鏈結串列 (Linked List) 為主
* 而為何要使用二元樹 ?
> 假設固定每個節點都有相同的分支度,假設 N 元樹每個節點有 M 連接,那此樹就使用了 N\*M 個位子...
> 2 元樹鏈結浪費率為 1/2
> 3 元樹鏈結浪費率為 1/3
> 4 元樹鏈結浪費率為 1/4
>
> 當 2 元樹時它的鏈結浪費率為最低,所以通常都使用 2 元樹
> ![](https://i.imgur.com/CiRqcJk.png)
* 二元樹每個節點只能連接 0~2 個分支樹,並且==有次序關係==
* **最大節點總和為 N = 2^K^-1** (K 為階度),如果節點為 N 可推出**高度 H = log~2~(N+1)** (N 為節點)
### 滿二元樹 (Fully Binary Tree)
* 2^K^-1 (K >= 0),K 為高度
> ![](https://i.imgur.com/Jl0SCew.png)
### 完整二元樹 (Complete)
* **必須依照次序**,由 **==左到右==** 排列
> ![](https://i.imgur.com/0etqdQk.png)
### 歪斜二元樹 (Skewed)
* 只有 left Node or right Node
> ![](https://i.imgur.com/BbUyfid.png)
### 嚴格二元樹 (Strictly)
* 每個非終端節點都有左右節點
> ![](https://i.imgur.com/YvmhXAC.png)
## 二元樹建立
* 建立二元樹的規則如下
> 1. 可以是空集合,若不是空結合,Node 上必須有 value
> 2. **left Node < Root < right Node**
> 3. 每個**節點的值必須要不同**
### 靜態
```java=
public class StaticBinaryTree {
public static void main(String[] args) {
int data[] = {6, 3, 5, 9, 7, 8, 4, 2};
for(int i = 0; i < data.length; i++) {
System.out.print("[" + data[i] + "] ");
}
System.out.println();
int[] result = makeBinaryTree(data);
for(int i = 0; i < result.length; i++) {
System.out.print("[" + result[i] + "] ");
}
System.out.println();
}
static int[] makeBinaryTree(int[] source) {
int nodeSize = binarySize(source.length);
System.out.println("Max Node Size is " + nodeSize);
int level;
int[] result = new int[nodeSize];
// 開始遍歷對象
for(int i = 0; i < source.length; i++) {
int putValue = source[i];
//"2. "
for(level = 1; result[level] != 0;) {
int resultValue = result[level];
//"3. "
if(putValue > resultValue) {
level = level * 2 + 1; // odd
} else {
level = level * 2; // even
}
}
result[level] = source[i];
}
return result;
}
//"1. "
static int binarySize(int x) {
return x * 2;
}
}
```
1. 一個節點必定連接兩個,所以需要的空間為 2 倍 (其實只需要 2^k^ -1)
2. `result[level] != 0` 的意思是,將要**放置的變數推送至最尾端再放置 (也就是要在新的陣列中找一個沒有放置過的點)**,放置**按照原本的順序放入**
3. ==放置的指標,以基偶數移動==,移動完後跳出迴圈並放入
> ![](https://i.imgur.com/VbouE81.png)
* 看靜態 2 元樹的可以看出,會浪費空間在放置 0
**--實作--**
> ![](https://i.imgur.com/y0ckRsy.png)
### 動態
* 使用 Linked 建置 2 元樹可以更省去空間
```java=
public class DynamicBinaryTree {
public static void main(String[] args) {
int[] a = {5, 8, 9, 6, 3, 1, 24, 7};
LinkedBinary l = new LinkedBinary(a);
}
}
class BinaryNode {
BinaryNode left;
final int value;
BinaryNode right;
public BinaryNode(int value) {
this.value = value;
}
}
class LinkedBinary {
BinaryNode root;
LinkedBinary(int[] Source) {
for(int i = 0; i < Source.length; i++) {
addToBinary(Source[i]);
}
}
void addToBinary(int data) {
BinaryNode n = new BinaryNode(data);
//"1."
if(root == null) {
root = n;
return;
}
//"2. "
BinaryNode realNode = root;
while(true) { // 找到末端
if(realNode.value > n.value) { // put to left
if(realNode.left != null) {
realNode = realNode.left;
} else {
realNode.left = n;
System.out.println("Put " + n.value + " to " + realNode.value + "'s left");
break;
}
} else { // put to right
if(realNode.right != null) {
realNode = realNode.right;
} else {
realNode.right = n;
System.out.println("Put " + n.value + " to " + realNode.value + "'s right");
break;
}
}
}
}
}
```
1. 建立根節點
2. 從根結點往下,搜尋到最底端 while(true),在放入資料
> ![](https://i.imgur.com/1upeR8M.png)
**--實作--**
> ![](https://i.imgur.com/AnFEQ7T.png)
## 遍歷二元樹
* 前、中、後走訪只須==記得樹根==的位子即可,再來就一定是左再右
> 前序 : Root -> Left -> Right
> 中序 : Left -> Root -> Right # 最常使用
> 後續 : Left -> Right -> Root
### 中序走訪
```java=
... //接續 LinkedBinary
void inOrder(BinaryNode b) {
if(b != null) {
inOrder(b.left);
System.out.println("Now Data is " + b.value);
inOrder(b.right);
}
}
```
**--實作--**
> ![](https://i.imgur.com/PFDsSNy.png)
### 前序走訪
```java=
... //接續 LinkedBinary
void preOrder(BinaryNode b) {
if(b != null) {
System.out.println("Now Data is " + b.value);
preOrder(b.left);
preOrder(b.right);
}
}
```
**--實作--**
> ![](https://i.imgur.com/oA15BEb.png)
### 後續走訪
```java=
... //接續 LinkedBinary
void postOrder(BinaryNode b) {
if(b != null) {
postOrder(b.left);
postOrder(b.right);
System.out.println("Now Data is " + b.value);
}
}
```
**--實作--**
> ![](https://i.imgur.com/jOBOx9r.png)
## 二元運算樹
* [**Stack**](https://hackmd.io/Cq9VpmG7RoORzqPKvAdHuA?view#算術運算式的求值) 也有運算堆疊
* 二元運算樹必須符合兩個規則
> 1. 考慮到運算式中的結合性,優先權,適當的加上**括號**
> 2. 利用運算子當樹根,左邊當左子樹,右邊當右子樹,優先權最低的運算子作為 Root
> > A-B\*(-C -3.5)
> > 1. 加上括號: A-(B*((-C)+(-3.5)))
> > 2. 由 (-C)+(-3.5) 開始往上畫
> > ![](https://i.imgur.com/i1jfCbY.png)
## 二元樹進階
二元排序樹、二元搜尋樹、引線二元樹
### 二元排序樹
1. 第一筆資料做為樹根
2. 後續資料與樹根做比較,小於樹根放左子樹,大於樹根放右子樹
* 使用 **==中序走訪==** 後就可以得到,==小 -> 大== 的排序
> Ex: 16, 2, 33, 14, 51, 1, 20, 15, 41
> Ans: 1, 2, 14, 15, 16, 20, 33, 41, 51
> ![](https://i.imgur.com/sbUYO71.png)
```java=
public class TestTree {
public static void main(String[] args) {
int[] list = {16, 2, 33, 14, 51, 1, 20, 15, 41};
SortTree sort = new SortTree(list);
sort.printSort(sort.root);
}
}
class TreeNode {
final int value;
TreeNode left, right;
TreeNode(int value) {
this.value = value;
}
}
class BinaryTree {
TreeNode root;
BinaryTree(int[] list) {
for(int i = 0; i < list.length; i++) {
addNode(list[i]);
}
}
void addNode(int data) {
TreeNode n = new TreeNode(data);
if(root == null) {
root = n;
return;
}
TreeNode temp = root;
while(true) {
if(temp.value > n.value) { // left tree
if(temp.left == null) {
temp.left = n;
break;
} else {
temp = temp.left;
}
} else if (temp.value < n.value) { // right tree
if(temp.right == null) {
temp.right = n;
break;
} else {
temp = temp.right;
}
}
}
}
}
class SortTree extends BinaryTree {
SortTree(int[] list) {
super(list);
}
// 小到大,中序走訪
void printSort(TreeNode temp) {
if (temp != null) {
printSort(temp.left);
System.out.println("value : " + temp.value);
printSort(temp.right);
}
}
}
```
**--實做--**
> ![](https://i.imgur.com/761RBPY.png)
### 二元搜尋樹
* **搜尋與排序是一體兩面的**,只要依照二元樹的規則走即可
1. 可是空集合,若不是每個 Node 上必須有值
2. 小於樹根放左子樹,大於樹根放右子樹
3. 左右子樹都是二元樹
```java=
public class TestTree {
public static void main(String[] args) {
int[] list = {16, 2, 33, 14, 51, 1, 20, 15, 41};
SearchTree search = new SearchTree(list);
search.findValue(20);
}
}
...// Tree 同上
class SearchTree extends BinaryTree {
SearchTree(int[] list) {
super(list);
}
void findValue(int v) {
if(root == null) {
System.out.println("Tree Empty");
return;
}
int count = 0;
TreeNode temp = root;
while(true) {
if(temp == null) {
System.out.print("Cannot find " + v + "in tree");
break;
}
System.out.println("Times: " + count++);
if(v == temp.value) {
System.out.println("find " + v + " in tree");
break;
} else if (v > temp.value) {
temp = temp.right;
} else if(v < temp.value) {
temp = temp.left;
}
}
}
}
```
**--實做--**
> ![](https://i.imgur.com/ooEEJ8w.png)
### 引線二元樹 (Threaded Binary Tree)
* 假設有 n 個節點的二元樹,其結點
> 真正有連結的結點為 n - 1 個
> 而沒有連結的結點有 n + 1 個
> > 假設 3 個節點,就有 2 個鏈結,4 個空鏈結
> > ![](https://i.imgur.com/rz9klvw.png)
* 步驟 :
1. 將二元樹由**中序**走法排出,並將所有**空鏈結改為==引線==**
2. 如果引線鏈結指向該節點的左方,將引線指到中序走訪的前一個節點
3. 如果引線鏈結指向該節點的右方,將引線指到中序走訪的後一個節點
4. 如果指向空結點 (頭、尾),將空結點的右鏈結指向自己
> 紅色為終端節點
> ![](https://i.imgur.com/v6OFg1A.png)
* Node 結構修改
> value: 節點資料
> right: 右子樹
> left: 左子樹
> rBit: 右控制位元,當為引線時 rBit = 0
> lBit: 左控制位元,當為引線時 lBit = 0
> ![](https://i.imgur.com/r7hp91u.png)
```java=
class ThreadedNode {
// 該點具體數值
final int value;
// 左右子樹
ThreadedNode right, left;
// 左右引線
boolean lBit, rBit;
ThreadedNode(int value) {
this.value = value;
}
}
```
**--實做--**
>
* 優點
> 1. 通常在二元樹**走訪**時都需要使用堆疊,而**引線二元樹則不須堆疊處理**
> 2. 有效利用每個鏈結,中序走訪時也較省時
> 3. 任一節點都容易找出前後關係
* 缺點
> 1. 加入、刪除實要處理的資料較多,導致速度較慢
> 2. 引線子樹不共享
## 樹 & 二元樹 表示
二元樹只是樹狀結構的特例,廣義的樹狀結構可有多個子節點 (並不限 2 個,一般情況下也並不會只有一種狀況),但是**二元樹的效率較高,所以將多元樹轉換二元樹就極其重要**
### 樹 & 二元樹轉換
* 樹轉化 -> 二元樹
> 1. 將結點的同梯度 (兄弟) 的連結
> 2. 刪除所有子節點的連接,但保留最左邊的連接
> 3. **平行節點==順時針==轉 45 度**
> ![](https://i.imgur.com/As844IU.png)
* 二元樹 -> 樹 (其實就是反向操作)
> 1. **==逆時針轉== 45 度**
> 2. 將所有子節點連接
> 3. 刪除同梯度 (兄弟) 連接線
### 樹林轉換二元樹
分開的樹林也可轉換為二元樹,其方法與樹轉二元樹類似,只是在最初多了一個步驟
* 樹林 -> 二元樹
> 1. **由左至右**將每棵樹的樹根 (root) 連接,這樣就以**最左邊的樹根為全部的根**
> 2. 將結點的同梯度 (兄弟) 的連結
> 3. 刪除所有子節點的連接,但保留最左邊的連接
> 4. **平行節點==順時針==轉 45 度**
> ![](https://i.imgur.com/mK6e9Hm.png)
* 二元樹 -> 樹林 (反推)
> 1. **==逆時針轉== 45 度**
> 2. 連接所有節點
> 3. 刪除同梯度 (兄弟) 的連結
> 4. 刪除根節點的連接 (就分開為樹林)
### 樹與樹林的走訪
`多元樹` & `樹林`都使用前、中、後序走訪,只是**走訪方法不同**
* ==多元樹走訪==
1. 中序走訪
> a. 先使用**中序走訪 (左根右)** 第一個子樹(B 子樹)
> b. 走訪樹根(A)
> c. 走訪接下來的子樹(C、D 子樹)
> ![](https://i.imgur.com/bd4Fyny.png)
> 上圖走訪 : EBFACGDHI
> 記法 : 先走訪其子樹
2. 前序走訪
> a. 訪問樹根 (A)
> b. 以**前序走訪 (根左右)** 依次訪問子樹 (B、C、D)
> ![](https://i.imgur.com/uGqoah4.png)
> 上圖走訪 : ABEFCDGHI
> 記法 : 先走訪樹根,同前序走法
3. 後序走訪
> a. 以**後序走訪 (左右根)** 依次訪問子樹 (B、C、D)
> b. 最後拜訪樹根
> ![](https://i.imgur.com/9ZmSwBE.png)
> 上圖走訪 : EFBCGHIDA
> 記法 : 先走訪棋子樹
* ==樹林走訪==
其走訪方式與多元樹走訪十分相像,也就是多元樹走訪的衍生
1. 中序走訪
> a. 以中序走訪訪問第一個**樹林的子樹群**
> b. 訪問樹根
> c. 以中序訪再走訪其他樹林
> 記法 : 先走訪其子樹
2. 前序走訪
> a. 顯訪問樹根
> b. 以前序依序走訪其他子樹
> 記法 : 先走訪樹根,同前序走法
3. 後序走訪
> a. 以後序依序走訪全部子樹
> b. 再到序串起樹根
> 記法 : 先走訪其子樹,到序串起樹根
### 唯一二元樹
透過**兩個排序(前、中、後序)的解可以得出唯一的二元樹**
* 排序的解有一個條件**必須**是 ==前、中序== or ==中、後序==,**如果只有++前後序那就無法得出++**
> 依序下列條件就可得出
> 1. 取每個樹中的 **最小節點** 為樹根,切開後左邊放左邊,右邊放右邊 (**不分大小放置左右**)
> 2. 由 **中序走訪** 的結果為準畫出
**--範例--**
> 中序走訪 : CBAEDFG
> 後序走訪 : ABGFCDE
>
> 唯一二元樹 ---- 起始點假設為 A,左右切開
> ![](https://i.imgur.com/Gmelv9i.png)
## 最佳化二元搜尋樹
> 在搜尋二元樹時可能有多個二元樹可以使用,而找出**最佳搜尋路徑找尋路徑就十分重要**,也就是找出 **==搜尋成本最低的二元樹==**
### 延伸二元樹 (Extension)
* 若有 N 個節點,就會有 N - 1 個非空節點,N + 1 個空節點,而在這些外節點上添加的節點稱為**外節點**,如下圖
* 畫出後就可以計算出 **內徑長(所有==內節點==到 Root 的總和)**,**外徑長(所有==外節點==到 Root 的總和)**
> ![](https://i.imgur.com/fpvfLFy.png)
> 內徑長(A ~ F to Root) : 1 + 2 + 3 + 1 + 2 + 2 = 11
> 外節點(a ~ h to Root) : 2 + 3 + 4 + 3 + 3 + 3 + 3 + 3 = 24
* 如果外部結點是 ==**加權值 (搜尋概率...)**== 就必須合併運算外徑長,使用**加權值 \* 內徑再相加**,又稱為加 **加權外徑長**
> ![](https://i.imgur.com/dqOnr2a.png)
> (加權 * 內徑) + ....
> 加權外徑長 : (1 * 1) + (4 * 2) + (6 * 3) + (1 * 3) + (3 * 2) + (8 * 2) + (2 * 2) + (5 * 2) => 1 + 8 + 18 + 3 + 6 + 16 + 4 + 10 = **66**
* 假設有兩個二元樹,互相比較外徑總和,==**最小外徑總和 (or 加權長度最小) 的為最佳二元樹**==
> 上面算出外徑為 24
> > 假設另一二元樹為 36,那上圖的二元樹就是最佳二元樹
> >
### 霍夫曼樹
* 霍夫曼樹經常用於**處理資料壓縮的問題,==根據資料出現的頻率來建構二元樹==**,每個節點都有外部節點,其節點為加權值
> 1. 對每一個節點產生兩個外部節點,並賦予這兩個節點該元素會出現的機率
> 2. 節點 N 有外部節點 T1、T2,**T1、T2 是機率出現最低的節點**,兩個加在一起就是 N 結點出現的機率
> 3. 消除步驟 1 & 2 插入 N,不斷重覆到機率 (權重) 消耗完畢
* 出現機率目前有 0.01、0.04、0.10、0.39、0.16、0.08、0.22,知道機率就可排出霍夫曼樹
> ![](https://i.imgur.com/0mLVt8L.png)
> 選重串列剩餘最小的兩個,重複步驟
> ![](https://i.imgur.com/YmzYRzz.png)
> 最後可得到結果,並得到 霍夫曼樹
> ![](https://i.imgur.com/ZAKhy5f.png)
## 平衡樹
> 二元搜尋樹的缺點就是無法保持最佳狀態 (平衡樹),在**加入資料的排序中時常會出現歪斜樹**,樹加高度增加就會導致效能下降 (權重變高),所以**保持在平衡樹是最佳狀態**
### 平衡樹定義
* T1 & T2 都是樹,它們的高度分別是 H1 & H2,|H1 - H2| <= 1 就是平衡樹 (最大差距為 1)
> ![](https://i.imgur.com/L7iF137.png)
## Appendix & FAQ
:::info
:::
###### tags: `資料結構`