我們知道二叉查找樹是一種數據結構,它支持多種動態集合的操作,包括:查詢,最大值,最小值,前驅,后繼,插入和刪除等操作。那么我們在前一篇已經創建了二叉查找樹,那么我們來實現二叉查找樹的各種操作吧。(*^__^*) (以下純屬個人理解,個人原創,理解不當的地方,請指正,謝謝)
我們來看二叉樹的遍歷操作,所謂遍歷,顧名思義,就是能夠依次的訪問二叉查找樹中的各個結點。在數據結構課堂上,我們知道,遍歷方式有深度優先和廣度優先遍歷,深度優先又包括前序、中序和后序遍歷;廣度優先遍歷,在二叉樹的范疇中,就是層序遍歷,就是先遍歷某一層的結點,然后再遍歷下一層的結點。好了,一個個的來介紹吧。
- 前序遍歷
前序遍歷,就是先遍歷父結點,然后遍歷左子樹,最后遍歷右子樹的遍歷方法,所謂前序,指的是遍歷父結點發生在遍歷左右子樹之前。
前序遍歷的遞歸算法很簡單,代碼如下:
1 /** 2 * 遞歸前序遍歷以x為根的二叉樹 3 * @author Alfred 4 * @param x 根結點 5 */ 6 public void preOrderTreeWalk(TreeNode x){ 7 if(x != null){ 8 System.out.print(x);//訪問形式為打印輸出一下 9 preOrderTreeWalk(x.getLeft()); 10 preOrderTreeWalk(x.getRight()); 11 } 12 }
這個算法,沒什么難點可說,就是先訪問當前結點,然后遞歸訪問當前結點的左子樹,最后遞歸訪問當前結點的右子樹。因為要遍歷所有節點,所以該算法的時間復雜度為O(n);
遞歸往往意味着低效,那么,如果不用遞歸的算法,如果實現前序遍歷呢?這里有兩個方法,都是借助“棧”來實現的。
方法1是模擬遞歸算法的實現效果,具體代碼如下:
1 /** 2 * 非遞歸前序遍歷以x為根結點的二叉樹 3 * @author Alfred 4 * @param x 根結點 5 */ 6 public void preOrderTreeWalkNonrecursive1(TreeNode x){ 7 //借助棧來實現。 8 Stack<TreeNode> stack = new Stack<TreeNode>(); 9 while(x != null || !stack.empty()){ 10 if(x != null){ 11 System.out.print(x);//遍歷輸出 12 stack.push(x);//壓棧 13 x = x.getLeft(); 14 }else{ 15 x = stack.pop();//出棧 16 x = x.getRight(); 17 } 18 } 19 }
方法1每次都將遇到的節點壓入棧,當左子樹遍歷完畢后才從棧中彈出最后一個訪問的節點,訪問其右子樹。在同一層中,不可能同時有兩個節點壓入棧,因此棧的大小空間為O(h),h為二叉樹高度。時間方面,每個節點都被壓入棧一次,彈出棧一次,訪問一次,復雜度為O(n)。
方法2是直接模擬遞歸來實現的,代碼如下:
/** * 非遞歸前序遍歷以x為根結點的二叉樹 * @author Alfred * @param x 根結點 */ public void preOrderTreeWalkNonrecursive2(TreeNode x){ Stack<TreeNode> stack = new Stack<TreeNode>(); if(x != null){ stack.push(x); while(!stack.empty()){ TreeNode tmpNode = stack.pop(); System.out.print(tmpNode);//遍歷輸出 if(tmpNode.getRight() != null){ stack.push(tmpNode.getRight()); } if(tmpNode.getLeft() != null){ stack.push(tmpNode.getLeft()); } } } }
方法2每次將節點壓入棧,然后彈出,壓右子樹,再壓入左子樹,在遍歷過程中,遍歷序列的右節點依次被存入棧,左節點逐次被訪問。同一時刻,棧中元素為m-1個右節點和1個最左節點,最高為h,所以空間也為O(h);每個節點同樣被壓棧一次,彈棧一次,訪問一次,時間復雜度O(n)。
- 中序遍歷
中序遍歷,就是先遍歷左子樹,然后遍歷父結點,最后遍歷右子樹,所謂中序,是指遍歷父結點在遍歷左子樹和遍歷右子樹之間。同時,根據二叉查找樹的性質,中序遍歷的輸出結果,恰好就是排序之后的序列。
中序遍歷的遞歸算法也很簡單,具體代碼如下:
1 /** 2 * 遞歸中序遍歷以x為根的二叉樹 3 * @author Alfred 4 * @param x 根結點 5 */ 6 public void inOrderTreeWalk(TreeNode x){ 7 if(x != null){ 8 inOrderTreeWalk(x.getLeft()); 9 System.out.print(x); 10 inOrderTreeWalk(x.getRight()); 11 } 12 }
先遞歸遍歷左子樹,然后訪問父結點,最后遞歸遍歷右子樹,上面的算法很簡單,不多說。
同理,非遞歸該如何實現二叉查找樹的中序遍歷呢?同樣,也需要借助“棧”來實現。代碼如下:
1 /** 2 * 非遞歸中序遍歷以x為根結點的二叉樹 3 * @author Alfred 4 * @param x 根結點 5 */ 6 public void inOrderTreeWalkNonrecursive(TreeNode x){ 7 Stack<TreeNode> stack = new Stack<TreeNode>(); 8 while(x != null || !stack.empty()){ 9 if(x != null){ 10 stack.push(x); 11 x = x.getLeft(); 12 }else{ 13 x = stack.pop(); 14 System.out.print(x);//遍歷輸出 15 x = x.getRight(); 16 } 17 } 18 }
該算法與前序遍歷的非遞歸算法1非常的相近,時間和空間復雜度也相同。至於對應的第二種非遞歸中序遍歷方法,沒有想到,牛逼的童鞋們自己來補充吧~
- 后續遍歷
后序遍歷,就是先遍歷左子樹,然后遍歷右子樹,最后遍歷父結點,所謂后序,是指遍歷父結點在遍歷左右子樹之后。
同樣,后序遍歷的遞歸算法也很簡單,代碼如下:
1 /** 2 * 遞歸后序遍歷以x為根的二叉樹 3 * @author Alfred 4 * @param x 根結點 5 */ 6 public void postOrderTreeWalk(TreeNode x){ 7 if(x != null){ 8 postOrderTreeWalk(x.getLeft()); 9 postOrderTreeWalk(x.getRight()); 10 System.out.print(x); 11 } 12 }
先遞歸遍歷左子樹,然后遞歸遍歷右子樹,最后父結點。
那么,后序遍歷的遞歸算法該如何寫呢?這里同樣有兩個方法,方法1用單棧實現,方法2用雙棧實現。
方法1代碼如下:
1 /** 2 * 非遞歸后序遍歷以x為根結點的二叉樹 3 * @author Alfred 4 * @param x 根結點 5 */ 6 public void postOrderTreeWalkNonrecursive1(TreeNode x){ 7 Stack<TreeNode> stack = new Stack<TreeNode>(); 8 TreeNode prev = null; 9 TreeNode curr = null; 10 if(x != null){ 11 stack.push(x); 12 } 13 while(!stack.empty()){ 14 curr = stack.peek(); 15 if(prev == null || prev.getLeft() == curr || prev.getRight() == curr){ 16 if(curr.getLeft() != null){ 17 stack.push(curr.getLeft());//壓左孩子 18 }else if(curr.getRight() != null){ 19 stack.push(curr.getRight());//壓右孩子 20 } 21 }else if(curr.getLeft() == prev){ 22 if(curr.getRight() != null){ 23 stack.push(curr.getRight());//壓右孩子 24 } 25 }else{ 26 System.out.print(curr);//遍歷輸出 27 stack.pop(); 28 } 29 prev = curr; 30 } 31 }
方法1是根據訪問前后元素的關系來進行的算法,根據關系的不同,發生的行為也就不同。
方法2的具體代碼為:
1 /** 2 * 非遞歸后序遍歷以x為根結點的二叉樹 3 * @author Alfred 4 * @param x 根結點 5 */ 6 public void postOrderTreeWalkNonrecursive2(TreeNode x){ 7 Stack<TreeNode> stack = new Stack<TreeNode>(); 8 Stack<TreeNode> output = new Stack<TreeNode>(); 9 TreeNode curr = null; 10 if(x != null){ 11 stack.push(x); 12 } 13 while(!stack.empty()){ 14 curr = stack.pop(); 15 output.push(curr);//存放到輸出地棧里面 16 if(curr.getLeft() != null){ 17 stack.push(curr.getLeft());//壓左孩子 18 } 19 if(curr.getRight() != null){ 20 stack.push(curr.getRight());//壓右孩子 21 } 22 } 23 while(!output.empty()){ 24 TreeNode tmpNode = output.pop(); 25 System.out.print(tmpNode);//打印輸出 26 } 27 }
方法2利用一個棧出棧並壓入到另一個棧的手法,對二叉查找樹進行了后序遍歷。
- 層序遍歷
層序遍歷,就是按照二叉樹的層次結構,逐層進行遍歷的方法。層序遍歷需要借助“隊列”來實現,具體的代碼如下:
1 /** 2 * 層序遍歷二叉樹 3 * @author Alfred 4 * @param x 根結點 5 */ 6 public void levelOrderTreeWalk(TreeNode x){ 7 Queue<TreeNode> queue = new LinkedList<TreeNode>(); 8 TreeNode tmpNode = null; 9 if(x != null){ 10 queue.offer(x); 11 } 12 while(!queue.isEmpty()){ 13 tmpNode = queue.poll(); 14 System.out.print(tmpNode);//打印輸出 15 if(tmpNode.getLeft() != null){ 16 queue.offer(tmpNode.getLeft());//左孩子入隊 17 } 18 if(tmpNode.getRight() != null){ 19 queue.offer(tmpNode.getRight());//右孩子入隊 20 } 21 } 22 }
上面的算法,先將根結點入隊,然后出隊,遍歷輸出,然后將左孩子和右孩子分別入隊,依次循環下去,知道隊列為空為止。
好了,遍歷就先寫到這兒吧。
ps:有寫的不好的,請各位童鞋指正。