二叉樹是常用的一種數據結構,今天記錄一下學習到的二叉樹的遍歷方法,其中包括遞歸方式和非遞歸方式的遍歷,這是在遍歷方法上的分類。在遍歷順序上分類,二叉樹的遍歷可以分為前序、中序、后序遍歷。所謂的前中后是指何時訪問中間節點,即前序遍歷,則遍歷節點的順序為:中-》左-》右;而中序遍歷,則遍歷節點的順序為:左-》中-》右;以此類推,后序遍歷的節點的順序為:左-》右-》中。
一般常用的二叉樹遍歷的方法為遞歸的方式,因為其思路簡單,且代碼實現也十分簡易,這里不再贅述,直接上代碼:
//后序遍歷,左=》右=》根 public void postorder_Traversal(TreeNode root) { if(root==null)return; postorder_Traversal(root.left); postorder_Traversal(root.right); //訪問節點的邏輯代碼塊 System.out.print(root.val+" "); } //前序遍歷,根=》左=》右 public void preorder_Traversal(TreeNode root) { if(root==null)return; //訪問節點的邏輯代碼塊 System.out.print(root.val+" "); preorder_Traversal(root.left); preorder_Traversal(root.right); } //中序遍歷,左=》根=》右 public void inorder_Traversal(TreeNode root) { if(root==null)return; inorder_Traversal(root.left); //訪問節點的邏輯代碼塊 System.out.print(root.val+" "); inorder_Traversal(root.right); }
接下來要講解的是非遞歸方式的遍歷二叉樹,相比遞歸方式,雖然非遞歸的方式遍歷二叉樹的思路比較復雜,並且實現也不易,但是非遞歸方式在效率方面,卻比遞歸方式高出不少,特別是當二叉樹十分龐大的時候,非遞歸方式可以很快的遍歷完全整棵樹,而遞歸方式則需要較長時間才能層層挖掘整棵樹,那么接下來挨個上代碼,通過代碼講解非遞歸方式。
//前序非遞歸遍歷,根=》左=》右 public void preorder(TreeNode root) { Stack<TreeNode> stack=new Stack<>(); while(root!=null||!stack.isEmpty()) { //當前節點不為空,則入棧,確保最后遍歷到的節點沒有左子節點 //因為是前序遍歷,所以再遍歷到每個節點的時候,都可以先訪問,再尋找其左右子節點。 while(root!=null) { System.out.print(root.val+" "); stack.push(root); root=root.left; } if(!stack.empty()) { //把這兩步看成是一步,找到右節點,並把已處理的中節點從stack當中去除 root=stack.pop(); root=root.right; } } }
從代碼當中可以看出,我們借助棧這個先進后出的特性,來存儲我們遍歷過的節點。過程如下:
1.每遍歷一個節點的時候,先節點入棧,然后尋找當前節點的左子節點。(因為是前序遍歷,所以在節點入棧之前就可以對節點進行訪問)
2.當某個節點的左子節點,當左子節點不為空的時候,重復過程1.
3.當左子節點為空的時候將當前節點出棧,並且通過其尋找右子節點,右子節點不為空的時候,重復過程1-2
4.當右子節點也為空的時候,則跳回上一個該節點的父節點(即因為當前節點已經出棧,所以現在在棧中最上層的節點是當前節點的父節點)
在了解完前序遍歷之后,我們再看中序遍歷代碼:
//中序遍歷非遞歸 public void Inorder(TreeNode root) { Stack<TreeNode> stack=new Stack<>(); while(root!=null||!stack.isEmpty()) { //當前節點不為空,則入棧,確保最后遍歷到的節點沒有左子節點 while(root!=null) { stack.push(root); root=root.left; } //通過當前節點,跳到當前節點的右節點,因為是中序遍歷,所以當前節點沒有左節點的時候,就可以訪問當前節點 if(!stack.empty()) { root=stack.pop(); System.out.print(root.val+" "); root=root.right; } } }
通過對比兩個方法,發現代碼幾乎一模一樣,但唯一的不同的是,訪問節點的位置不一樣,中序遍歷是當左子節點被訪問過,或者不存在的時候,才可以訪問中間節點,所以再該處,訪問節點的位置放在了當左子節點不存在的時候,即節點出棧的時候,即是左子節點不存在的時候進行訪問。
最后我們來看看為難的非遞歸后序遍歷,與前兩個方法不同,后序遍歷是需要左右子節點都遍歷過后,才能訪問中間節點,那么前邊的一個棧就無法滿足我們的要求了,因為我們需要一個標識位去確定是否是已經完成了左右子節點的遍歷,結合代碼,來理解這個方法:
//后序遍歷非遞歸 public void Postorder(TreeNode root) { Stack<TreeNode> s =new Stack<>(); Stack<Integer> s2 =new Stack<>(); Integer i=new Integer(1); while(root!=null||!s.isEmpty()) { //只要當前節點非空,就入棧 while(root!=null) { s.push(root); s2.push(new Integer(0)); root=root.left; } //s2當中如果存1,則意味着當前s1對應的節點的左右子節點都已經遍歷過了。 while(!s.empty()&&s2.peek().equals(i)) { s2.pop(); System.out.print(s.pop().val+" "); } if(!s.isEmpty()) { s2.pop(); s2.push(new Integer(1)); root=s.peek(); root=root.right; } } }
從代碼當中可以看出,我們借助兩個棧來存儲我們的節點以及標示位,過程如下:
1.每遍歷一個節點的時候,先節點入棧s,並且s2入棧一個標識位0,然后尋找當前節點的左子節點。
2.當某個節點的左子節點,當左子節點不為空的時候,重復過程1.
3.當左子節點為空的時候將當前節點peek出(即將節點拿出,但棧中還是有該節點),並且此時將s2對應棧頂的標識位改為1,通過其尋找右子節點,右子節點不為空的時候,重復過程1-2
4.當右子節點也為空的時候,並且s2對應的標識符為1的時候,則彈出s1棧頂的當前節點,並且將s2的標識符彈出(即因為當前節點還沒有出棧,所以現在在棧中最上層的節點是當前節),注意s1彈出當前節點並訪問,但是不賦值給root,在這個root此時還是null
5.進入過程3,此時root被peek賦值到當前節點的父節點(因為在過程4當中,已經pop出了當前節點,所以s1棧頂是當前節點的父節點)的右子節點。
6.重復過程1-5
可能看了這個過程講解還是有點不清楚,但是這個流程通過多讀幾次代碼,相信未來的自己還是能讀懂的。
至此,二叉樹的遍歷就講解完畢。
