【基礎演算法】:線索二叉樹
重拾演算法(2)——線索二叉樹
上一篇我們實現了二叉樹的遞迴和非遞迴遍歷,併為其複用精心設計了遍歷方法Traverse(TraverseOrder order, NodeWorker<T> worker);今天就在此基礎上實現線索二叉樹。
什麼是線索二叉樹
二叉樹中容易找到結點的左右孩子資訊,但該結點在某一序列中的直接前驅和直接後繼只能在某種遍歷過程中動態獲得。
先依遍歷規則把每個結點某一序列中對應的前驅和後繼線索預存起來,這叫做"線索化"。
意義:從任一結點出發都能快速找到其某一序列中前驅和後繼,且不必藉助堆疊。
這就是線索二叉樹(Threaded Binary Tree)
資料結構
如何預存這類資訊?有兩種解決方法。
每個結點增加兩個域:fwd和bwd
與原有的左右孩子指標域"複用"
任意一個二叉樹,其結點數為N,則有N+1個空鏈域。空鏈域就是葉結點的lchild和rchild。比如下面這個二叉樹
它有7個結點,有(7+1)個空鏈域。這個結論可以用數學歸納法證明。
大體思路就是充分利用那n+1個空鏈域,用它儲存前驅結點和後繼結點。
我們規定:
- 若結點有左子樹,則lchild指向其左孩子;否則,lchild指向其直接前驅(即線索);
- 若結點有右子樹,則rchild指向其右孩子;否則,rchild指向其直接後繼(即線索) 。
那麼就有如下一個問題:如何判斷是孩子指標還是線索指標?
容易的很,加兩個標誌域:
標誌域只需要2個bit,而用增加指標域的方式則需要2個指標的空間(一個指標就是一個int的長度)。所以這個方案極大地節省了空間。
我們規定:
- 當Tag域為0時,表示孩子情況;
- 當Tag域為1時,表示線索情況。
有關線索二叉樹的幾個術語
線索連結串列:用含Tag的結點樣式所構成的二叉連結串列。
線 索:指向結點前驅和後繼的指標。
線索二叉樹:加上線索的二叉樹。
線 索 化:對二叉樹以某種次序遍歷使其變為線索二叉樹的過程。
線索化過程就是在遍歷過程中修改空指標的過程:
- 將空的lchild改為結點的直接前驅;
-
將空的rchild改為結點的直接後繼;
- 非空指標呢?仍然指向孩子結點。(稱為"正常情況")
C#實現線索二叉樹
資料結構
線索二叉樹是(is a)二叉樹。所以最初我想繼承上一篇的二叉樹BinaryTreeNode<T>型別。但後來發現程式碼不太好掌控,不如來得直接些,讓二叉樹和線索二叉樹作為兩個毫不相干的型別。以後重構的時候再考慮考慮如何整合。
1 public partial class ThreadedBinaryTreeNode<T> 2 { 3 public T Value { get;set; } 4 public ThreadedBinaryTreeNode<T> Parent { get;set; } 5 public ThreadedBinaryTreeNode<T> Left { get;set; } 6 public ThreadedBinaryTreeNode<T> Right { get;set; } 7 public bool Point2PreviousNode { get; private set; } 8 public bool Point2NextNode { get; private set; } 9 public TraverseOrder Order { get;set; } 10 private bool threadedOnce = false; 11 12 public ThreadedBinaryTreeNode(T value, ThreadedBinaryTreeNode<T> parent = null, ThreadedBinaryTreeNode<T> left = null, ThreadedBinaryTreeNode<T> right = null) 13 { 14 this.Value = value; this.Parent = parent; this.Left = left; this.Right = right; 15 } 16 17 public void Traverse(TraverseOrder order, ThreadedNodeWorker<T> worker) 18 { 19 if (worker == null) { return; } 20 21 if ((!threadedOnce) || (order != this.Order)) 22 { 23 var router = new Router<T>(worker); 24 this.NormalTraverse(order, router); 25 this.Order = order; 26 this.threadedOnce = true; 27 } 28 else 29 { 30 this.ThreadedTraverse(order, worker); 31 } 32 } 33 } 34 35 public abstract class ThreadedNodeWorker<T> 36 { 37 public abstract void DoActionOnNode(ThreadedBinaryTreeNode<T> node); 38 }
C#裡用bool型別作為標誌變數大概再好不過了。
線索化
線索二叉樹的線索化是首先要解決的問題。
1 public partial class ThreadedBinaryTreeNode<T> 2 { 3 /* 略 */ 4 private void RefreshThread(TraverseOrder order) 5 { 6 var threader = new Threader<T>(); 7 this.NormalTraverse (order, threader); 8 this.Order = order; 9 } 10 11 class Threader<TThreader> : ThreadedNodeWorker<TThreader> 12 { 13 Queue<ThreadedBinaryTreeNode<TThreader>> queue = new Queue<ThreadedBinaryTreeNode<TThreader>>(); 14 public override void DoActionOnNode(ThreadedBinaryTreeNode<TThreader> node) 15 { 16 this.queue.Enqueue(node); 17 if (this.queue.Count <= 1) { return; } 18 19 var pre = this.queue.Dequeue(); 20 var next = this.queue.Peek(); 21 if (pre.Right == null) 22 { 23 pre.Right = next; 24 pre.Point2NextNode = true; 25 } 26 if (next.Left == null) 27 { 28 next.Left = pre; 29 next.Point2PreviousNode = true; 30 } 31 } 32 } 33 }
程式碼中的this.NormalTraverse()函式與上一篇中二叉樹的遍歷方法Traverse()思路是一樣的,唯一區別在於傳入的操作結點的物件threader,這個threader在用前序、中序、後序或層次遍歷二叉樹時,將結點存入一個佇列queue,當queue中有2個結點時,就設定他們互為前驅後驅(若此2個結點的Left/Right為空的前提下)。簡單來說,就是遍歷二叉樹,根據訪問結點的先後順序依次修改其前驅後驅指標(及標識)
完成線索化後,就可以遍歷了。
前序遍歷
前序遍歷線索二叉樹的思路是:對於一個結點A及其左右子樹Left和Right,先訪問A;如果A的標記Point2NextNode為true,則A的下一結點通過A.Right就能確定;否則,有兩種情況:(1)A的下一結點在A.Left上,而以A.Left為根的這個子樹的第一個被訪問的結點,恰恰是A.Left這個結點。(2)A.Left為空,這說明A的下一結點在A.Right上。若A.Right也為空,就說明整個樹遍歷結束。
1 void ThreadedPreorderTraverse(ThreadedNodeWorker<T> worker) 2 { 3 var node = this; 4 while (node != null) 5 { 6 var p2next = node.Point2NextNode; 7 var p2pre = node.Point2PreviousNode; 8 var right = node.Right; 9 var left = node.Left; 10 worker.DoActionOnNode(node); 11 if (p2next) 12 { 13 node = right; 14 } 15 else 16 { 17 if (p2pre) 18 { 19 node = right; 20 } 21 else 22 { 23 if (left != null) 24 { node = left; } 25 else 26 { node = right; } 27 } 28 } 29 } 30 }
中序遍歷
中序遍歷線索二叉樹的思路是:對於一個結點A及其左右子樹Left和Right,第一個要訪問的結點在A.Left子樹,A.Left子樹中第一個要訪問的結點則在A.Left.Left子樹中,依次迴圈,直到最後一個葉結點的Left。所以A樹要訪問的第一個結點就用GetLeftMostNode()這個函式去找。訪問完A,就該A的右子樹了,所以A.Right就成為下一個A了,所以就用GetLeftMostNode(A.Right)獲取下一個結點。
1 ThreadedBinaryTreeNode<T> GetLeftMostNode(ThreadedBinaryTreeNode<T> node) 2 { 3 if (node == null) { return null; } 4 while ((node.Left != null) && (!node.Point2PreviousNode)) 5 { 6 node = node.Left; 7 } 8 return node; 9 } 10 11 void ThreadedInorderTraverse(ThreadedNodeWorker<T> worker) 12 { 13 var node = GetLeftMostNode(this); 14 while (node != null) 15 { 16 var p2next = node.Point2NextNode; 17 var p2pre = node.Point2PreviousNode; 18 var right = node.Right; 19 var left = node.Left; 20 worker.DoActionOnNode(node); 21 if (p2next) 22 { 23 node = right; 24 } 25 else 26 { 27 node = GetLeftMostNode(right);// visit right sub tree. 28 } 29 } 30 }
後續遍歷
後序遍歷線索二叉樹的思路是:對於一個結點A及其左右子樹Left和Right,第一個要訪問的結點在A.Left子樹,A.Left子樹中第一個要訪問的結點則在A.Left.Left子樹中,依次迴圈,直到最後一個葉結點的Left。所以A樹要訪問的第一個結點就用GetLeftMostNode()這個函式去找。訪問完A樹,下一步分三種情況:(1)A是整個樹的根,這時整個遍歷結束。(2)A樹是A.Parent的左子樹,這時就該A.Parent.Right了,於是A.Parent.Right就成了之前的A。(3)A樹是A.Parent的右子樹,這時就該A結點了。
1 ThreadedBinaryTreeNode<T> GetFirstNode4PostorderTraverse(ThreadedBinaryTreeNode<T> root) 2 { 3 if (root == null) { return null; } 4 5 ThreadedBinaryTreeNode<T> result = null; 6 var stack = new Stack<ThreadedBinaryTreeNode<T>>(); var stackReady4Visit = new Stack<bool>(); 7 8 stack.Push(root); stackReady4Visit.Push(false); 9 10 while (stack.Count > 0) 11 { 12 var node = stack.Pop(); var ready4Visit = stackReady4Visit.Pop(); 13 //if (node == null) { continue; } 14 if (ready4Visit) 15 { 16 result = node; 17 break; 18 } 19 else 20 { 21 var right = node.Right; 22 var left = node.Left; 23 var p2pre = node.Point2PreviousNode; 24 var p2next = node.Point2NextNode; 25 stack.Push(node); stackReady4Visit.Push(true); 26 if ((right != null) && (!p2next)) { stack.Push(right); stackReady4Visit.Push(false); } 27 if ((left != null) && (!p2pre)) { stack.Push(left); stackReady4Visit.Push(false); } 28 } 29 } 30 return result; 31 } 32 33 void ThreadedPostorderTraverse(ThreadedNodeWorker<T> worker) 34 { 35 var node = GetFirstNode4PostorderTraverse(this); 36 37 while (node != null) 38 { 39 var p2next = node.Point2NextNode; 40 var p2pre = node.Point2PreviousNode; 41 var right = node.Right; 42 var left = node.Left; 43 worker.DoActionOnNode(node); 44 if (p2next) 45 { 46 node = right; 47 } 48 else 49 { 50 var parent = node.Parent; 51 if (parent != null)// node is NOT root 52 { 53 if (parent.Right == node)// node is right sub tree of parent 54 { 55 node = parent; 56 } 57 else // node is left sub tree of parent 58 { 59 if (parent.Point2NextNode) 60 { 61 node = parent; 62 } 63 else 64 { 65 node = GetFirstNode4PostorderTraverse(parent.Right); 66 if (node == null) 67 { 68 node = parent; 69 } 70 } 71 } 72 } 73 else// node is root of whole tree 74 { 75 node = parent; 76 } 77 } 78 } 79 }
層次遍歷
線索二叉樹對於層次遍歷似乎沒有優化的能力,其層次遍歷仍舊用普通二叉樹的遍歷方法好了。
1 void NormalLayerTraverse(ThreadedNodeWorker<T> worker) 2 { 3 var queue = new Queue<ThreadedBinaryTreeNode<T>>(); 4 queue.Enqueue(this); 5 while (queue.Count > 0) 6 { 7 var node = queue.Dequeue(); 8 if (node != null) 9 { 10 var left = node.Left; 11 var right = node.Right; 12