二叉樹中序遍歷的三種方式
leetcode(94 Binary Tree Inorder Traversal) 題目:
Given a binary tree, return the inorder traversal of its nodes' values.
Example:
Input: [1,null,2,3] 1 \ 2 / 3 Output: [1,3,2]
一棵樹的中序遍歷分為三步驟:
- 先遍歷其左子樹
- 再遍歷本身節點
- 最後遍歷其右子樹
以下圖為例,中序遍歷的結果為 [4,2,5,1,6,3,7]
先定義樹的結構:
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */
1.遞迴方式:
public List<Integer> inorderTraversal(TreeNode root) { List<Integer> result = new ArrayList<>(); helper(root,result); return result; } public void helper(TreeNode root,List<Integer> result){ if(root != null){ if(root.left != null) helper(root.left,result); result.add(root.val); if(root.right !=null) helper(root.right,result); } }
2.迭代方式:
public List<Integer> inorderTraversal(TreeNode root) { if(root == null) return new ArrayList<Integer>(); List<Integer> result = new ArrayList<>(); Stack<TreeNode> stack = new Stack(); TreeNode temp = root; while(temp !=null || !stack.isEmpty()){ while(temp != null){ stack.push(temp); temp =temp.left; } temp = stack.pop(); result.add(temp.val); temp =temp.right; } return result; }
3.莫里斯遍歷方式:
public List<Integer> inorderTraversal(TreeNode root) {
if(root == null)
return new ArrayList<>();
TreeNode cur = root; //當前節點的位置
List<Integer> res = new ArrayList<>();
while(cur != null){
if(cur.left ==null){
res.add(cur.val);
cur = cur.right;
}else{
TreeNode pre = cur.left;
while(pre.right != null && pre.right !=cur){ //尋找左子樹的最右節點
pre = pre.right;
}
if(pre.right == null){ //返回父結點
pre.right =cur;
cur = cur.left;
}else{ // 已遍歷過,需要斷開連線
pre.right = null;
res.add(cur.val); //中序遍歷存放當前節點
cur = cur.right;
}
}
}
return res;
}
前兩中方式程式碼一目瞭然,後一種方式需要稍稍闡述下原理與步驟:
原理:
遞迴或者棧的空間複雜度都是O(n),假設我們需要O(1)空間複雜度的遍歷,是否有一種方式可以實現,答案是可以的。我們可以使用線索二叉樹(threaded binary tree)的概念,利用葉子節點的空指標尋找其前序後繼節點,左指標指向前驅節點,右指標指向後繼節點,這樣做到了O(n)時間複雜度,O(1)空間複雜度的遍歷。
如上圖所示,利用葉子節點的空指標指向其前序後繼節點(紅色標識),假如我們想中序遍歷,只需要先尋找出第一個左子樹為null的節點,然後按照線索二叉樹的線索進行判斷是後繼節點還是右子樹,若是後繼節點,則直接遍歷(放入res中) 然後跳轉到其右子樹:
public List<Integer> threadedTreeListByInOrder() {
HeroNode node = root;
List<Integer> res = new ArrayList<>();
while (node != null) {
while (node.getLeftType() == 0) {
node = node.left;
}
res.add(node.val);
while (node.getRightType() == 1) {
node = node.right;
res.add(node.val);
}
node = node.right;
}
return res;
}
但這裡有一個限制(你在程式碼裡也注意到了),就是在定義樹結構的時候,需要宣告兩個線索,用於判斷其左指標是前驅節點還是左子樹:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* int leftType; //判斷左指標的類別
* int rightType; //判斷右指標的類別
* TreeNode(int x) { val = x; }
* }
*/
這樣本質上,就改變了樹形結構的定義。那有沒有一種既用了線索二叉樹的原理,又不改變其定義呢?答案依然是有的,就是本文第三種遍歷方式:Morris Traversal
Morris Traversal:通過動態建立線索的方法進行遍歷,遍歷某個節點完後,將該節點相關聯的線索(姑且這麼稱呼吧)進行刪除,遍歷完後,不改變樹的結構和原本的資料分佈
實現原則分為2大步:首先記當前節點為cur
-
1:若
cur
的左子節點為null
,遍歷該節點,當前節點cur
指向其右子節點 -
2:若
cur
的左子節點不為null
,設pre
為cur
的左子節點,pre
為一個臨時節點 遍歷尋找pre
下的最右側葉節點,並複製給pre
[pre =pre.right]
- 2.1:若
pre
的右子節點 為null
, 將當前節點cur
賦值給pre
的右子節點[pre.right=cur]
,cur
移到自身的的左子節點[cur = cur.left]
- 2.2:若
pre
的右子節點為cur
,表示pre
到cur
已經連線了,現在需要遍歷然後結束該連線,所以,錄入cur
節點[res.add(cur.val)]
,並將pre
的右子節點置空[pre.right=null]
,之後cur移到自身的右子節點[cur =cur.right]
重複上述的2大步直到節點
cur
為空如果前面的原則你有點迷糊,不要緊,我們以上面的例子做個全過程,幫助理解下 ,其中
res
存放遍歷後的節點 : - 2.1:若
morris遍歷過程:
-
(1):首先cur 從頭節點 1 開始,按照前面morris原則的第二步,它存在左子節點,先搜尋左子節點的最右側節點並賦值給pre,該節點為 5 ,再根據最右側葉節點的值為
null
,所以將 5 的右子節點指向1,cur
移動到自身的左子節點 2res=[]
-
(2):2 有左子節點,且2的左子節點4 按照morris原則第二步,先搜尋左子節點的最右側節點,根據最右側葉節點的值為
null
,按照2.1原則,**所以將 4 的右子節點指標指向 2 ,cur
移到自身的左子節點 4res=[]
-
(3):4沒有左子節點,按照morris原則第一步,遍歷該節點並將cur指向該節點的右子節點 (在上一步中,我們已經將4的右指標指向了2,所以我們可以直接進行跳轉至4的右子節點2)
res=[4]
-
(4):
cur
此時回到了2, 2有左子節點,按照morris原則的第二步,搜尋左子節點的最右側節點,發現在搜尋過程中4的右子節點指向了cur
,按照前面的2.2原則,4的右指標指向null
,遍歷當前節點,同時cur
指向其右子節點 5res=[4,2]
-
(5):5不存在右子節點,按照morris原則的第一步,遍歷該節點並將cur指向該節點的右子節點(根據流程的第一步,我們已經將5節點的右指標指向了1,所以
cur
跳轉至1)res=[4,2,5]
-
(6):cur此時回到了1,按照morris原則的第二步,搜尋左子節點的最右側節點,發現在搜尋過程中5的右子節點指向了
cur
,按照前面的2.2原則,5的右指標指向null
,遍歷當前節點,同時cur
指向其右子節點 3(這一步與流程的第4步一樣)res=[4,2,5,1]
-
(7):3有左子節點7,按照morris原則第二步,先搜尋左子節點的最右側節點,根據最右側葉節點的值為
null
,按照2.1原則所以將 6 的右子節點指標指向 3 ,cur
移到自身的左子節點 6res=[4,2,5,1]
- (8):6沒有左子節點,按照morris原則的第一步,遍歷該節點並將
cur
指向該節點的右子節點res=[4,2,5,1,6]
-
(9):cur此時回到了3,有左子節點6,依然是搜尋左子節點下的最右側節點,搜尋過程中發現右側節點指向cur本身,按照morris原則的2.2 ,6的右指標指向
null
,遍歷當前節點,同時cur
指向其右子節點 7res=[4,2,5,1,6,3]
-
(10) :7沒有左子節點,直接遍歷並將
cur
指向該節點的右子節點 ,最後cur
指向了null
,此時整個中序遍歷完成。res=[4,2,5,1,6,3,7]
空間複雜度:O(1),比較好推斷
時間複雜度:O(n)。可能會疑問以下程式碼跟樹的高度有關:
while(pre.right != null && pre.right !=cur) //尋找左子樹的最右節點
pre = pre.right;
但事實上,尋找所有節點的前驅節點只需要O(n)時間。以上面的流程為例,尋找前驅節點中所有的節點最多被訪問了兩遍!加上自身的遍歷,總共最多方法3遍。所以,樹的每個節點最多方法3遍,時間複雜度為O(n)。