一、線索二叉樹原理
前面介紹二叉樹原理及特殊二叉樹文章中提到,二叉樹可以使用兩種儲存結構:順序儲存和二叉鏈表。在使用二叉鏈表的儲存結構的過程中,會存在大量的null 指標域,為了充分利用這些null 指標域,引申出了“線索二叉樹”。回顧一下二叉鏈表格儲存體結構,如下圖:
通過觀察上面的二叉鏈表,存在著若干個沒有指向的null 指標域。對於一個有n個節點的二叉鏈表,每個節點有指向左右節點的2個指標域,整個二叉鏈表存在2n個指標域。而n個節點的二叉鏈表有n-1條分支線,那麼null 指標域的個數=2n-(n-1) = n+1個null 指標域,從儲存空間的角度來看,這n+1個null 指標域浪費了記憶體資源。
從另外一個角度來分析,如果我們想知道按中序方式遍曆二叉鏈表時B節點的前驅節點或者後繼節點時,必須要按中序方式遍曆二叉鏈表才能夠知道結果,每次需要結果時都需要進行一次遍曆,是否可以考慮提前儲存這種前驅和後繼的關係來提高時間效率呢。
綜合以上兩方面的分析,可以通過充分利用二叉鏈表中的null 指標域,存放節點在某種遍曆方式下的前驅和後繼節點的指標。 我們把這種指向前驅和後繼的指標成為線索,加上線索的二叉鏈表成為線索鏈表,對應的二叉樹就成為“線索二叉樹(Threaded Binary Tree)” 。
二、構建線索二叉樹過程
1、我們對二叉樹進行中序遍曆(不瞭解二叉樹遍曆請參考二叉樹及特殊二叉樹介紹),將所有的節點右子節點為空白的指標域指向它的後繼節點。如下圖:
通過中序遍曆我們知道H的right指標為空白,並且H的後繼節點為D(如上圖第1步),I的right指標為空白,並且I的後繼節點為B(如上圖第2步),以此類推,知道G的後繼節點為null,則G的right指標指向null。
2、接下來將這顆二叉樹的所有節點左指標域為空白的指標域指向它的前驅節點。如下圖:
如上圖,H的left指標域指向Null(如第1步),I的前驅節點是D,則I的left指標指向D,以此類推。
通過上面兩步完成了整個二叉樹的線索化,最後結果如下圖:
通過觀察上圖(藍色虛線代表後繼、綠色虛線代表前驅),可以看出,線索二叉樹,等於是把一棵二叉樹轉變成了一個“ 特殊的雙向鏈表“(後面會解釋為什麼叫特殊的雙向鏈表),這樣對於我們的新增、刪除、尋找節點帶來了方便。所以我們對二叉樹以某種次序遍曆使其變為線索二叉樹的過程稱做是線索化。如下圖:
仔細分析上面的雙向鏈表,與線索化之後的二叉樹相比,比如節點D與後繼節點I,在完成線索化之後,並沒有直接線索指標,而是存在父子節點的指標;節點A與節點F,線上索化完成之後,節點A並沒有直接指向後繼節點F的線索指標,而是通過父子節點遍曆可以找到最終的節點F,前驅節點也存在同樣的問題,正因為很多節點之間不存在直接的線索,所以我將此雙向鏈表稱做“ 特殊的雙向鏈表”,再使用過程中根據指標是線索指標還是子節點指標來分別處理,所以在每個節點需要標明當前的左右指標是線索指標還是子節點指標,這就需要修改節點的資料結構。修改後的資料結構如下:
class Node { String data; //資料域 Node left; //左指標域 Node right; //右指標域 byte leftType; //左指標域類型 0:指向子節點、1:前驅或後繼線索 byte rightType; //右指標域類型 0:指向子節點、1:前驅或後繼線索 }
最終的二叉鏈表修改為如下圖的樣子:
三、線索二叉樹的代碼(Java版)
下面是中序線索化二叉樹的實現代碼:
/** * @Title: 二叉樹相關操作 * @Description: * @Author: Uncle Ming * @Date:2017年1月6日 下午2:49:14 * @Version V1.0 */public class ThreadBinaryTree { private Node preNode; //線索化時記錄前一個節點 //節點儲存結構 static class Node { String data; //資料域 Node left; //左指標域 Node right; //右指標域 boolean isLeftThread = false; //左指標域類型 false:指向子節點、true:前驅或後繼線索 boolean isRightThread = false; //右指標域類型 false:指向子節點、true:前驅或後繼線索 Node(String data) { this.data = data; } } /** * 通過數組構造一個二叉樹(完全二叉樹) * @param array * @param index * @return */ static Node createBinaryTree(String[] array, int index) { Node node = null; if(index < array.length) { node = new Node(array[index]); node.left = createBinaryTree(array, index * 2 + 1); node.right = createBinaryTree(array, index * 2 + 2); } return node; } /** * 中序線索化二叉樹 * @param node 節點 */ void inThreadOrder(Node node) { if(node == null) { return; } //處理左子樹 inThreadOrder(node.left); //左指標為空白,將左指標指向前驅節點 if(node.left == null) { node.left = preNode; node.isLeftThread = true; } //前一個節點的後繼節點指向當前節點 if(preNode != null && preNode.right == null) { preNode.right = node; preNode.isRightThread = true; } preNode = node; //處理右子樹 inThreadOrder(node.right); } /** * 中序遍曆線索二叉樹,按照後繼方式遍曆(思路:找到最左子節點開始) * @param node */ void inThreadList(Node node) { //1、找中序遍曆方式開始的節點 while(node != null && !node.isLeftThread) { node = node.left; } while(node != null) { System.out.print(node.data + ", "); //如果右指標是線索 if(node.isRightThread) { node = node.right; } else { //如果右指標不是線索,找到右子樹開始的節點 node = node.right; while(node != null && !node.isLeftThread) { node = node.left; } } } } /** * 中序遍曆線索二叉樹,按照前驅方式遍曆(思路:找到最右子節點開始倒序遍曆) * @param node */ void inPreThreadList(Node node) { //1、找最後一個節點 while(node.right != null && !node.isRightThread) { node = node.right; } while(node != null) { System.out.print(node.data + ", "); //如果左指標是線索 if(node.isLeftThread) { node = node.left; } else { //如果左指標不是線索,找到左子樹開始的節點 node = node.left; while(node.right != null && !node.isRightThread) { node = node.right; } } } } /** * 前序線索化二叉樹 * @param node */ void preThreadOrder(Node node) { if(node == null) { return; } //左指標為空白,將左指標指向前驅節點 if(node.left == null) { node.left = preNode; node.isLeftThread = true; } //前一個節點的後繼節點指向當前節點 if(preNode != null && preNode.right == null) { preNode.right = node; preNode.isRightThread = true; } preNode = node; //處理左子樹 if(!node.isLeftThread) { preThreadOrder(node.left); } //處理右子樹 if(!node.isRightThread) { preThreadOrder(node.right); } } /** * 前序走訪線索二叉樹(按照後繼線索遍曆) * @param node */ void preThreadList(Node node) { while(node != null) { while(!node.isLeftThread) { System.out.print(node.data + ", "); node = node.left; } System.out.print(node.data + ", "); node = node.right; } } public static void main(String[] args) { String[] array = {"A", "B", "C", "D", "E", "F", "G", "H"}; Node root = createBinaryTree(array, 0); ThreadBinaryTree tree = new ThreadBinaryTree(); tree.inThreadOrder(root); System.out.println("中序按後繼節點遍曆線索二叉樹結果:"); tree.inThreadList(root); System.out.println("\n中序按後繼節點遍曆線索二叉樹結果:"); tree.inPreThreadList(root); Node root2 = createBinaryTree(array, 0); ThreadBinaryTree tree2 = new ThreadBinaryTree(); tree2.preThreadOrder(root2); tree2.preNode = null; System.out.println("\n前序按後繼節點遍曆線索二叉樹結果:"); tree.preThreadList(root2); }}
四、小結 線索化的實質就是將二叉鏈表中的null 指標改為指向前驅節點或後繼節點的線索; 線索化的過程就是修改二叉鏈表中null 指標的過程,可以按照前序、中序、後序的方式進行遍曆,分別產生不同的線索二叉樹; 有了線索二叉樹之後,我們再次遍曆時,就相當於操作一個雙向鏈表。 使用情境:如果我們在使用二叉樹過程中經常需要遍曆二叉樹或者尋找節點的前驅節點和後繼節點,可以考慮採用線索二叉樹儲存結構。
PS: 未完待續…
線索二叉樹的原理、前序和中序線索化本文已經儘可能做了詳細的描述,對於後序線索化二叉樹比較相對複雜,為了避免引起不適,用單獨的文章進行了分析,需要的同學請轉向後序線索化二叉樹。