# 線索二元樹(Threaded Binary Tree) 一個二元樹通過線索連起來。所有原本為空的右(孩子)指針改為指向該節點在中序序列中的後繼,所有原本為空的左(孩子)指針改為指向該節點的中序序列的前驅。 假設該二元樹中序遍歷為 `A B C D E F G H I`,根據上述說明試著解讀為何這些節點的線索是這樣連的。 ![](https://i.imgur.com/kbdDBah.png) A 節點的左指針為空,因此指向其中序的前驅節點為 B,右指針要指向後繼,但它是第一個遍歷所以為空;C 節點的左指針指向前驅 B,右指針指向後繼 D...以此類推。 :::warning - 線索二元樹能線性地遍歷二元樹,從而比遞迴的中序遍歷更快,並且建立線索並不影響時間複雜度。 - 使用線索二元樹也能夠方便的找到一個節點的父節點,這比多宣告一個父親節點指針來找 或者用 棧 效率更高。 - 這在棧空間有限、無法使用存儲父節點的棧時很有作用(對於通過深度優先搜索DFS來搜尋父節點而言)。 ::: 因此,當使用線索二元樹時,Node節點的left和right屬性有以下情況需要考慮: 1. left 指向的是左子樹,也可能是指向前驅節點。 2. right 指向的是右子樹,也可能是指向後繼節點。 ## 簡易實現 試著找出 10 的前驅和後繼節點為何? ![](https://i.imgur.com/5zThnC7.png) 首先我們需要在節點類中宣告兩個變數,用於將左指針右指針分類,並加上get和set方法。 ```java= // 1. 如果 leftType = 0 表示指向左子樹 , leftType = 1 表示指向前驅節點 // 2. 如果 rightType = 0 表示指向右子樹 , rightType = 1 表示指向後繼節點 private int leftType; private int rightType; public int getLeftType() { return leftType; } public void setLeftType(int leftType) { this.leftType = leftType; } public int getRightType() { return rightType; } public void setRightType(int rightType) { this.rightType = rightType; } ``` 接著需要在樹類宣告一個 prev 節點用於指向當前節點的前驅節點 ```java= private TreeNode prev = null; ``` 線索化的方法 ```java= // 重載threadedNodes方法 public void threadedNodes() { this.threadedNodes(root); } /** * * @param node 當前需要線索化的節點 */ //對二元樹進行中序線索化的方法 public void threadedNodes(TreeNode node) { if(node == null) { return; } // 1. 先線索化左子樹 threadedNodes(node.getLeft()); // 2. 線索化當前節點 // 處理當前節點的前驅節點 if(node.getLeft() == null) { // 讓當前節點的左指針指向前驅節點 node.setLeft(prev); // 修改當前節點的左指針類型 node.setLeftType(1); } // 處理後繼節點 if(prev != null && prev.getRight() == null) { // 讓前驅節點的右指針指向當前節點 prev.setRight(node); // 修改當前節點的右指針類型 prev.setRightType(1); } // 每處理一個節點後, 讓當前節點是下一個節點的前驅節點 prev = node; // 3. 再線索化右子樹 threadedNodes(node.getRight()); } ``` 把二元樹建立好,然後線索化,輸出 10 的前驅和後繼節點。 ```java= public static void main(String[] args) { TreeNode root = new TreeNode(1, "Tom"); TreeNode node2 = new TreeNode(3, "Jack"); TreeNode node3 = new TreeNode(6, "Smith"); TreeNode node4 = new TreeNode(8, "Mary"); TreeNode node5 = new TreeNode(10, "King"); TreeNode node6 = new TreeNode(14, "Candy"); // 簡單處理使用手動創建 root.setLeft(node2); root.setRight(node3); node2.setLeft(node4); node2.setRight(node5); node3.setLeft(node6); // 測試中序線索化 ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree(); threadedBinaryTree.setRoot(root); threadedBinaryTree.threadedNodes(); // 測試, 以 10 測試 // 原本 10 的左指針為空 , 線索化後左指針指向前驅節點 3 TreeNode leftNode = node5.getLeft(); System.out.println("10號節點的前驅節點為: "+leftNode); // 原本 10 的右指針為空 , 線索化後右指針指向後繼節點 1 TreeNode rightNode = node5.getRight(); System.out.println("10號節點的後繼節點為: "+rightNode); } ``` output ```java= 10號節點的前驅節點為: TreeNode [no = 3, name = Jack ] 10號節點的後繼節點為: TreeNode [no = 1, name = Tom ] ``` ## 線索二元樹的遍歷 ![](https://i.imgur.com/5zThnC7.png) 在前面二元樹的部分有提到過遍歷,但是因為線索化後各個節點指向有變化,因此原來的遍歷方式不能使用,需要使用新的方式遍歷線索化二元樹。各個節點可以通過**線性方式**遍歷,無需使用遞迴方式,提高了遍歷的效率。遍歷的次序應當和中序遍歷保持一致。 ### 中序遍歷 在樹類中寫一個遍歷方法 ```java= // 遍歷線索化二元樹的方法 public void threadedList() { // 定義一個變數, 臨時存儲當前遍歷的節點, 從root開始 TreeNode node = root; while(node != null) { // 循環的找到leftType = 1 的節點, 說明該節點是按照線索化處理後的有效節點 while(node.getLeftType() == 0) { node = node.getLeft(); } // 輸出當前節點 System.out.println(node); // 如果當前節點的右指針指向的是後繼節點, 就一直輸出 while(node.getRightType() == 1) { node = node.getRight(); System.out.println(node); } // 替換這個遍歷的節點 node = node.getRight(); } } ``` 測試中序遍歷是否正確,為8, 3, 10, 1, 14, 6 ```java= System.out.println("===使用線索化方式遍歷線索化二元樹==="); threadedBinaryTree.threadedList(); //8, 3, 10, 1, 14, 6 ``` output ```java= ===使用線索化方式遍歷線索化二元樹=== TreeNode [no = 8, name = Mary ] TreeNode [no = 3, name = Jack ] TreeNode [no = 10, name = King ] TreeNode [no = 1, name = Tom ] TreeNode [no = 14, name = Candy ] TreeNode [no = 6, name = Smith ] ``` ### 前序遍歷 前序線索化 ```java= //對二元樹進行前序線索化的方法 public void preThreadedNodes(TreeNode node) { if(node == null) { return; } if(node.getLeft() == null) { // 讓當前節點的左指針指向前驅節點 node.setLeft(prev); // 修改當前節點的左指針類型 node.setLeftType(1); } if(prev != null && prev.getRight() == null) { // 讓前驅節點的右指針指向當前節點 prev.setRight(node); // 修改當前節點的右指針類型 prev.setRightType(1); } prev = node; if(node.getLeftType() == 0) { preThreadedNodes(node.getLeft()); } if(node.getRightType() == 0) { preThreadedNodes(node.getRight()); } } ``` 前序遍歷 ```java= // 前序遍歷線索化二元樹 public void preThreadedList() { TreeNode node = root; while(node != null) { System.out.println(node); while(node.getLeftType() == 0) { node = node.getLeft(); System.out.println(node); } while(node.getRightType() == 1) { node = node.getRight(); System.out.println(node); } node = node.getRight(); } } ``` 試著遍歷看看是否正確 ```java= System.out.println("===前序==="); threadedBinaryTree.preThreadedNodes(); threadedBinaryTree.preThreadedList(); //前:1, 3, 8, 10, 6, 14 ``` output ```java= ===前序=== TreeNode [no = 1, name = Tom ] TreeNode [no = 3, name = Jack ] TreeNode [no = 8, name = Mary ] TreeNode [no = 10, name = King ] TreeNode [no = 6, name = Smith ] TreeNode [no = 14, name = Candy ] ``` 完整程式碼在 [ThreadedBinaryTreeDemo.java](https://github.com/AquariusMay/Data-Structures-and-Algorithms-in-Java/blob/master/BinaryTree/ThreadedBinaryTreeDemo.java)