題目描述(中等難度)
二叉樹的中序遍歷。
解法一 遞歸
學二叉樹的時候,必學的算法。用遞歸寫簡潔明瞭,就不多說了。
package inary_Tree_Inorder_Traversal;
import java.util.ArrayList;
import java.util.List;
class TreeNode{
int val;
TreeNode left;
TreeNode right;
TreeNode(int val){
this.val=val;
}
}
public class inary_Tree_Inorder_Traversal1 {
public static List<Integer> inorderTraversal(TreeNode root){
List<Integer> ans=new ArrayList<>();
getAns(root,ans);
return ans;
}
private static void getAns(TreeNode node, List<Integer> ans) {
if(node==null) return ;
getAns(node.left,ans);
ans.add(node.val);
getAns(node.right,ans);
}
public static void main(String[] args) {
int n=5;
TreeNode[] node = new TreeNode[n];//以數組形式生成一棵完全二叉樹
for(int i = 0; i < n; i++) node[i] = new TreeNode(i);
for(int i = 0; i < n; i++){
if(i*2+1 < n) node[i].left = node[i*2+1];
if(i*2+2 < n) node[i].right = node[i*2+2];
}
List<Integer> ans=inorderTraversal(node[0]);
System.out.println(ans);
}
}
時間複雜度:O(n),遍歷每個節點。
空間複雜度:O(h),壓棧消耗,h 是二叉樹的高度。
官方解法中還提供了兩種解法,這裏總結下。
解法二 棧
利用棧,去模擬遞歸。遞歸壓棧的過程,就是保存現場,就是保存當前的變量,而解法一中當前有用的變量就是 node,所以我們用棧把每次的 node 保存起來即可。
模擬下遞歸的過程,只考慮 node 的壓棧。
//當前節點爲空,出棧
if (node == null) {
return;
}
//當前節點不爲空
getAns(node.left, ans); //壓棧
ans.add(node.val); //出棧後添加
getAns(node.right, ans); //壓棧
//左右子樹遍歷完,出棧
看一個具體的例子,想象一下吧。
1
/ \
2 3
/ \ /
4 5 6
push push push pop pop push pop pop
| | | | |_4_| | | | | | | | | | |
| | |_2_| |_2_| |_2_| | | |_5_| | | | |
|_1_| |_1_| |_1_| |_1_| |_1_| |_1_| |_1_| | |
ans add 4 add 2 add 5 add 1
[] [4] [4 2] [4 2 5] [4 2 5 1]
push push pop pop
| | | | | | | |
| | |_6_| | | | |
|_3_| |_3_| |_3_| | |
add 6 add 3
[4 2 5 1 6] [4 2 5 1 6 3]
結合代碼。
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
//節點不爲空一直壓棧
while (cur != null) {
stack.push(cur);
cur = cur.left; //考慮左子樹
}
//節點爲空,就出棧
cur = stack.pop();
//當前值加入
ans.add(cur.val);
//考慮右子樹
cur = cur.right;
}
return ans;
}
時間複雜度:O(n)。
空間複雜度:O(h),棧消耗,h 是二叉樹的高度。
解法三 Morris Traversal
解法一和解法二本質上是一致的,都需要 O(h)的空間來保存上一層的信息。而我們注意到中序遍歷,就是遍歷完左子樹,然後遍歷根節點。如果我們把當前根節點存起來,然後遍歷左子樹,左子樹遍歷完以後回到當前根節點就可以了,怎麼做到呢?
我們知道,左子樹最後遍歷的節點一定是一個葉子節點,它的左右孩子都是 null,我們把它右孩子指向當前根節點存起來,這樣的話我們就不需要額外空間了。這樣做,遍歷完當前左子樹,就可以回到根節點了。
當然如果當前根節點左子樹爲空,那麼我們只需要保存根節點的值,然後考慮右子樹即可。
所以總體思想就是:記當前遍歷的節點爲 cur。
1、cur.left 爲 null,保存 cur 的值,更新 cur = cur.right
2、cur.left 不爲 null,找到 cur.left 這顆子樹最右邊的節點記做 last
2.1 last.right 爲 null,那麼將 last.right = cur,更新 cur = cur.left
2.2 last.right 不爲 null,說明之前已經訪問過,第二次來到這裏,表明當前子樹遍歷完成,保存 cur 的值,更新 cur = cur.right
結合圖示:
如上圖,cur 指向根節點。 當前屬於 2.1 的情況,cur.left 不爲 null,cur 的左子樹最右邊的節點的右孩子爲 null,那麼我們把最右邊的節點的右孩子指向 cur。
接着,更新 cur = cur.left。
如上圖,當前屬於 2.1 的情況,cur.left 不爲 null,cur 的左子樹最右邊的節點的右孩子爲 null,那麼我們把最右邊的節點的右孩子指向 cur。
更新 cur = cur.left。
如上圖,當前屬於情況 1,cur.left 爲 null,保存 cur 的值,更新 cur = cur.right。
如上圖,當前屬於 2.2 的情況,cur.left 不爲 null,cur 的左子樹最右邊的節點的右孩子已經指向 cur,保存 cur 的值,更新 cur = cur.right。
如上圖,當前屬於情況 1,cur.left 爲 null,保存 cur 的值,更新 cur = cur.right。
如上圖,當前屬於 2.2 的情況,cur.left 不爲 null,cur 的左子樹最右邊的節點的右孩子已經指向 cur,保存 cur 的值,更新 cur = cur.right。
cur 指向 null,結束遍歷。
根據這個關係,寫代碼
記當前遍歷的節點爲 cur。
1、cur.left 爲 null,保存 cur 的值,更新 cur = cur.right
2、cur.left 不爲 null,找到 cur.left 這顆子樹最右邊的節點記做 last
2.1 last.right 爲 null,那麼將 last.right = cur,更新 cur = cur.left
2.2 last.right 不爲 null,說明之前已經訪問過,第二次來到這裏,表明當前子樹遍歷完成,保存 cur 的值,更新 cur = cur.right
public List<Integer> inorderTraversal3(TreeNode root) {
List<Integer> ans = new ArrayList<>();
TreeNode cur = root;
while (cur != null) {
//情況 1
if (cur.left == null) {
ans.add(cur.val);
cur = cur.right;
} else {
//找左子樹最右邊的節點
TreeNode pre = cur.left;
while (pre.right != null && pre.right != cur) {
pre = pre.right;
}
//情況 2.1
if (pre.right == null) {
pre.right = cur;
cur = cur.left;
}
//情況 2.2
if (pre.right == cur) {
pre.right = null; //這裏可以恢復爲 null
ans.add(cur.val);
cur = cur.right;
}
}
}
return ans;
}
時間複雜度:O(n)。每個節點遍歷常數次。
空間複雜度:O(1)。
參考文章
1.https://zhuanlan.zhihu.com/p/70887882
2.https://leetcode-cn.com/problems/binary-tree-inorder-traversal/