# 二元樹(Binary Tree) 在提二元樹之前簡單提一下樹結構,因為這本筆記比較注重實作部分,所以概念的部分建議自己補上。 ## 樹結構 ![](https://i.imgur.com/xL9whAb.png) > 圖片來源 維基百科 - 樹狀結構 * 在樹狀結構中的基本單位,稱為**節點(Node)**。 * 節點之間的連結,稱為**分支(branch)**。 * 節點與分支形成樹狀,結構的開端,稱為**根(root)**,或根節點。 * 根節點之外的節點,稱為**子節點(child)**。 * 沒有連結到其他子節點的節點,稱為**葉子節點(Leaf)**。 也就是說,雖然稱之為樹,但是其上下顛倒,根節點指的是最上面資料的開頭,而葉子節點是最下面沒有子節點的資料。 ### 定義與特性 1. 由 1 個以上的節點所構成的集合,不可為空。 2. 至少有一個特殊節點,稱為根節點。 3. 一個節點可以有多個子節點。 4. 無左右次序。 ### 為什麼需要樹結構 我們先來回顧一下之前提到過的儲存方式 #### ⚔【陣列】 * **優點**:可以通過索引訪問元素,速度快,對於有序陣列還可以用二元搜尋找提高搜尋速度。 * **缺點**:當要搜尋具體某個值,或者插入、刪除元素會整體移動,效率較低。 因為陣列大小是固定的,若想要插入新的值,需要陣列擴容:每次在底層都需要創建新陣列,並將原來的陣列數據 copy 回去並插入新的數據。 ![](https://i.imgur.com/8brcdSu.png) :::warning ArrayList 底層維護了一個 `Object[]` 型別的陣列 elementData,所以仍然是進行了陣列擴容。 ::: #### ⚔【鏈結串列】 * **優點**:一定程度上對陣列儲存方式有優化,插入和刪除元素只需要修改鏈結即可。 * **缺點**:在進行搜尋時效率仍然較低,比如要搜尋某個值,需要從頭節點開始遍歷。 ![](https://i.imgur.com/BFITfSW.png) :::danger 🌵陣列🌵 - 訪問快 - 插入和刪除效率低 🌵鏈結串列🌵 - 插入刪除效率高 - 搜尋效率低 ::: #### ⚔【樹】 能提高數據存儲、讀取的效率,比如利用二元排序樹(Binary Sort Tree),既可以保證數據的搜尋速度,同時也可以保證數據的插入、刪除、修改的速度。 **為什麼呢?** 假設我要搜尋 `12` 這個元素的位置,我已經知道根節點為 7 ,我將 12 與 7 比較,發現 12 > 7 所以我只需要往右子樹去找,直接不需要去找左子樹,省下了不少時間。 ![](https://i.imgur.com/UipSgLr.png) ## 二元樹 Binary Search Tree、Heap、Huffman Tree、BSP Tree這些通通都是二元樹,二元樹的應用相當廣泛,在今年前後端和資料工程師LeetCode 面試調查中一致選出幫助最大的演算法就是二元樹。 樹分很多種,二元樹是每個節點最多只有兩個分支的樹結構,節點左邊稱為左子樹,節點右邊稱為右子樹,二元樹的分支具有左右次序,不能隨意顛倒。 比如有一陣列 `[7, 3, 10, 1, 5, 9, 12]`,我們將 7 設為根節點,比 7 小的放在左子樹,比 7 大的元素放在右子樹,擺放過程如下: ![](https://i.imgur.com/VvxZuhE.png) 1 < 7 且 1 < 3 放在最左,5 < 7 但 5 > 3 所以放在 3 的右子樹 ![](https://i.imgur.com/gUilzH3.png) 9 > 7 但 9 < 10 放在 10 的左子樹,12 > 7 也 > 10 放在最右,此時我們的二元樹長成這個樣子: ![](https://i.imgur.com/k33UAVV.png) ### 定義與特性 1. 每個節點的分支數 <= 2。 2. 二元樹的節點個數是一個有限集合,或是沒有節點的空集合。二元樹的節點可以分成兩個沒有交集的子樹,稱為「左子樹」(Left Subtree)和「右子樹」(Right Subtree)。 3. 若該二元樹的所有葉子節點都在最後一層,並且節點總數為 `2^n^-1`,n 為層樹,則稱為**完滿二元樹(Full Binary Tree)**。 ![](https://i.imgur.com/EHceIid.png) 5. 若該二元樹的所有葉子節點都在最後一層或者倒數第二層,而且最後一層的葉子節點在左邊連續,倒數第二層的葉子節點在右邊連續,稱為**完整二元樹(Complete Binary Tree)**。 ![](https://i.imgur.com/RIc7gT5.png) > 圖片來源 [資料結構的樹與二元樹](http://wayne.cif.takming.edu.tw/datastru/tree.pdf) ## 二元樹的遍歷 二元樹的遍歷分為三種順序: * 前序:**先輸出父節點**,再遍歷左子樹和右子樹。 * 中序:先遍歷左子樹,**再輸出父節點**,再遍歷右子樹。 * 後序:先遍歷左子樹,再遍歷右子樹,**最後輸出父節點**。 **小結**:看輸出父節點的順序就能確定是**前序、中序**還是**後序**。 ### 思路 1. 創建一顆二元樹 2. **前序遍歷** 2.1 先輸出當前節點(初始的時候是根節點) 2.2 如果左子節點不為空,則遞迴前序遍歷 2.3 如果右子節點不為空,則遞迴前序遍歷 3. **中序遍歷** 3.1 如果當前節點的左子節點不為空,則遞迴中序遍歷 3.2 輸出當前節點 3.3 如果當前節點的右子節點不為空,則遞迴中序遍歷 4. **後序遍歷** 4.1 如果當前節點的左子節點不為空,則遞迴後序遍歷 4.2 如果當前節點的右子節點不為空,則遞迴後序遍歷 4.3 輸出當前節點 ### 程式碼實現 有一二元樹如圖所示,我們要對它進行前序、中序、後序遍歷 ![](https://i.imgur.com/0Y3xERO.png) ```java= package BinaryTree; public class BinaryTreeDemo { public static void main(String[] args) throws Exception { BinaryTree binaryTree = new BinaryTree(); HeroNode root = new HeroNode(1, "宋江"); HeroNode node2 = new HeroNode(2, "吳用"); HeroNode node3 = new HeroNode(3, "盧俊義"); HeroNode node4 = new HeroNode(4, "林沖"); root.setLeft(node2); root.setRight(node3); node3.setRight(node4); binaryTree.setRoot(root); System.out.println("===前序遍歷==="); binaryTree.preOrder(); System.out.println("===中序遍歷==="); binaryTree.inOrder(); System.out.println("===後序遍歷==="); binaryTree.postOrder(); } } class BinaryTree { private HeroNode root; public void setRoot(HeroNode root) { this.root = root; } public void preOrder() { if(this.root != null) { this.root.preOrder(); } else { System.out.println("二元樹為空"); } } public void inOrder() { if(this.root != null) { this.root.inOrder(); } else { System.out.println("二元樹為空"); } } public void postOrder() { if(this.root != null) { this.root.postOrder(); } else { System.out.println("二元樹為空"); } } } // 先創建 HeroNode 節點 class HeroNode { private int no; private String name; private HeroNode left; private HeroNode right; public HeroNode(int no,String name) { this.no = no; this.name = name; } public int getNo() { return no; } public void setNo(int no) { this.no = no; } public String getName() { return name; } public void setName(String name) { this.name = name; } public HeroNode getLeft() { return left; } public void setLeft(HeroNode left) { this.left = left; } public HeroNode getRight() { return right; } public void setRight(HeroNode right) { this.right = right; } @Override public String toString() { return "HeroNode [no = " + no + ", name = " + name + " ]"; } public void preOrder() { System.out.println(this); if(this.left != null) { this.left.preOrder(); } if(this.right != null) { this.right.preOrder(); } } public void inOrder() { if(this.left != null) { this.left.inOrder(); } System.out.println(this); if(this.right != null) { this.right.inOrder(); } } public void postOrder() { if(this.left != null) { this.left.postOrder(); } if(this.right != null) { this.right.postOrder(); } System.out.println(this); } } ``` output ```java= ===前序遍歷=== HeroNode [no = 1, name = 宋江 ] HeroNode [no = 2, name = 吳用 ] HeroNode [no = 3, name = 盧俊義 ] HeroNode [no = 4, name = 林沖 ] ===中序遍歷=== HeroNode [no = 2, name = 吳用 ] HeroNode [no = 1, name = 宋江 ] HeroNode [no = 3, name = 盧俊義 ] HeroNode [no = 4, name = 林沖 ] ===後序遍歷=== HeroNode [no = 2, name = 吳用 ] HeroNode [no = 4, name = 林沖 ] HeroNode [no = 3, name = 盧俊義 ] HeroNode [no = 1, name = 宋江 ] ``` ### 運算式的二元樹 補充一個運算式的二元樹,使用二元樹前序後序遍歷應該就能快速的得知前綴後綴如何表式。 中綴為:`[((6 + 4) / 2) * (9 + (4 - 3))]` ![](https://i.imgur.com/KndPXUl.png) 前序:`* / + 6 4 2 + 9 - 4 3` 即為前綴表示法 後序:`6 4 + 2 / 9 4 3 - + *` 即為後綴表示法 ## 二元樹的搜尋 ### 思路 #### 前序搜尋 1. 先判斷當前節點的 no 是否等於要搜尋的。 2. 如果相等 返回當前節點。 3. 如果不等,則判斷當前節點的左子節點是否為空,如果不為空,則遞迴前序搜尋。 4. 如果左遞迴搜尋,找到節點則返回,否則繼續判斷,當前節點的右子節點是否為空,如果不空,則繼續向右遞迴前序搜尋。 #### 中序搜尋 1. 判斷當前節點的左子節點是否為空,如果不為空,則遞迴中序搜尋。 2. 如果找到,則返回,如果沒找到就和當前節點比較,如果是則返回當前節點,否則繼續進行右遞迴的中序搜尋。 3. 如果右遞迴中序搜尋,找到就返回,否則返回 null。 #### 後序搜尋 1. 判斷當前節點的左子節點是否為空,如果不為空,則遞迴後序搜尋。 2. 如果找到,則返回,如果沒找到就判斷當前節點的右子節點是否為空,如果不為空則右遞迴進行後序搜尋,如果找到就返回。 3. 就和當前節點進行比較,比如,如果是則返回,否則返回 null。 ### 程式碼實現 HeroNode 類別 ```java= // 前序遍歷搜尋 public HeroNode preOrderSearch(int no) { System.out.println("前序遍歷 1 次"); // 計算遍歷了幾次才找到 if(this.no == no) { return this; } // 1.判斷當前節點的左子節點是否為空, 如果不為空則遞迴前序搜尋 // 2.如果左遞迴前序搜尋, 找到節點就返回 HeroNode resNode = null; if(this.left != null) { resNode = this.left.preOrderSearch(no); } if(resNode != null) { // 說明左子樹找到 return resNode; } // 1.左遞迴前序搜尋, 找到節點就返回, 否則繼續判斷 // 2.當前節點的右子節點是否為空, 如果不空, 則繼續向右遞迴前序搜尋 if(this.right != null) { resNode = this.right.preOrderSearch(no); } return resNode; } // 中序遍歷搜尋 public HeroNode infixOrderSearch(int no) { // 判斷當前節點的左子節點是否為空, 如果不為空則遞迴中序搜尋 HeroNode resNode = null; if(this.left != null) { resNode = this.left.infixOrderSearch(no); } if(resNode != null) { return resNode; } System.out.println("中序遍歷 1 次"); // 計算遍歷了幾次才找到 // 如果找到則返回, 沒找到就和當前節點比較, 如果是則返回當前節點 if(this.no == no) { return this; } // 否則繼續右遞迴中序搜尋 if(this.right != null) { resNode = this.right.infixOrderSearch(no); } return resNode; } // 後序遍歷搜尋 public HeroNode postOrderSearch(int no) { // 判斷當前節點的左子節點是否為空, 如果不為空則遞迴後序搜尋 HeroNode resNode = null; if(this.left != null) { resNode = this.left.postOrderSearch(no); } if(resNode != null) { return resNode; } // 否則繼續右遞迴後序搜尋 if(this.right != null) { resNode = this.right.postOrderSearch(no); } if(resNode != null) { return resNode; } System.out.println("後序遍歷 1 次"); // 計算遍歷了幾次才找到 if(this.no == no) { return this; } return resNode; } ``` BinaryTree 類別 ```java= // 前序搜尋 public HeroNode preOrderSearch(int no) { if(root != null) { return root.preOrderSearch(no); } else { return null; } } // 中序搜尋 public HeroNode infixOrderSearch(int no) { if(root != null) { return root.infixOrderSearch(no); } else { return null; } } // 後序搜尋 public HeroNode postOrderSearch(int no) { if(root != null) { return root.postOrderSearch(no); } else { return null; } } ``` 試著前序搜尋 ```java= System.out.println("~~~前序搜尋~~~"); HeroNode resNode = binaryTree.preOrderSearch(5); if(resNode != null) { System.out.printf("訊息為 no = %d , name = %s\n",resNode.getNo(),resNode.getName()); } else { System.out.println("找不到資訊"); } System.out.println("~~~中序搜尋~~~"); resNode = binaryTree.infixOrderSearch(5); if(resNode != null) { System.out.printf("訊息為 no = %d , name = %s\n",resNode.getNo(),resNode.getName()); } else { System.out.println("找不到資訊"); } System.out.println("~~~後序搜尋~~~"); resNode = binaryTree.postOrderSearch(5); if(resNode != null) { System.out.printf("訊息為 no = %d , name = %s\n",resNode.getNo(),resNode.getName()); } else { System.out.println("找不到資訊"); } ``` output ```javascript= ~~~前序搜尋~~~ 前序遍歷 1 次 前序遍歷 1 次 前序遍歷 1 次 前序遍歷 1 次 訊息為 no = 5 , name = 關勝 ~~~中序搜尋~~~ 中序遍歷 1 次 中序遍歷 1 次 中序遍歷 1 次 訊息為 no = 5 , name = 關勝 ~~~後序搜尋~~~ 後序遍歷 1 次 後序遍歷 1 次 訊息為 no = 5 , name = 關勝 ``` ## 二元樹的刪除 ### 要求 1. 如果刪除的節點是葉子節點,則刪除該節點 2. 如果刪除的節點是非葉子節點,則刪除該子樹 3. 測試,刪除掉 5號葉子節點 和 3號子樹 ![](https://i.imgur.com/fHmreuR.png) ### 思路 1. 考慮如果樹是空樹,或只有一個root節點,則等於將二元樹置空。 2. 因為二元樹是單向的,所以我們是判斷當前節點的子節點是否需要刪除節點,而不能去判斷當前這個節點是不是需要刪除節點。 3. 如果當前節點的左子節點不為空,並且左子節點就是要刪除節點,就將`this.left = null;` 並且就返回(結束遞迴刪除)。 4. 如果當前節點的右子節點不為空,並且右子節點就是要刪除節點,就將`this.right = null;` 並且就返回(結束遞迴刪除)。 5. 如果第三第四步都沒有刪除節點,就需要向左子樹進行遞迴刪除。 6. 如果第五步也沒有刪除節點,則應向右子樹進行遞迴刪除。 首先需要先處理第一點,如果這個二元樹並非空樹或者僅有一個root節點才需要去考慮 2 ~ 6 步。 ### 程式碼實現 HeroNode 類加上一個刪除方法 ```java= // 遞迴刪除節點 public void delNode(int no) { // 如果當前節點的左子節點不為空,並且左子節點就是要刪除節點,就將 this.left = null; 並且就返回(結束遞迴刪除) if(this.left != null && this.left.no == no) { this.left = null; return; } // 如果當前節點的右子節點不為空,並且右子節點就是要刪除節點,就將this.right = null; 並且就返回(結束遞迴刪除) if(this.right != null && this.right.no == no) { this.right = null; return; } // 如果前面兩步都沒有刪除節點,就需要向左子樹進行遞迴刪除 if(this.left != null) { this.left.delNode(no); } // 如果上一步也沒有刪除節點,則應向右子樹進行遞迴刪除 if(this.right != null) { this.right.delNode(no); } } ``` BinaryTree 類也加上一個刪除方法 ```java= // 刪除節點 public void delNode(int no) { if(root != null) { // 如果只有一個root節點, 需要判斷root是不是就是要刪除的節點 if(root.getNo() == no) { root = null; } else { // 遞迴刪除 root.delNode(no); } } else { System.out.println("空樹"); } } ``` 來試試刪除葉子節點 和 非葉子節點有什麼區別: ```java= public static void main(String[] args) throws Exception { BinaryTree binaryTree = new BinaryTree(); HeroNode root = new HeroNode(1, "宋江"); HeroNode node2 = new HeroNode(2, "吳用"); HeroNode node3 = new HeroNode(3, "盧俊義"); HeroNode node4 = new HeroNode(4, "林沖"); HeroNode node5 = new HeroNode(5, "關勝"); root.setLeft(node2); root.setRight(node3); node3.setRight(node4); node3.setLeft(node5); binaryTree.setRoot(root); System.out.println("===前序遍歷==="); binaryTree.preOrder(); binaryTree.delNode(5); System.out.println("===刪除 5 後前序遍歷==="); binaryTree.preOrder(); binaryTree.delNode(3); System.out.println("===刪除 3 後前序遍歷==="); binaryTree.preOrder(); } ``` output ```java= ===前序遍歷=== HeroNode [no = 1, name = 宋江 ] HeroNode [no = 2, name = 吳用 ] HeroNode [no = 3, name = 盧俊義 ] HeroNode [no = 5, name = 關勝 ] HeroNode [no = 4, name = 林沖 ] ===刪除 5 後前序遍歷=== HeroNode [no = 1, name = 宋江 ] HeroNode [no = 2, name = 吳用 ] HeroNode [no = 3, name = 盧俊義 ] HeroNode [no = 4, name = 林沖 ] ===刪除 3 後前序遍歷=== HeroNode [no = 1, name = 宋江 ] HeroNode [no = 2, name = 吳用 ] ``` ![](https://i.imgur.com/fHmreuR.png)