關於二叉樹的定義,以及什么是二叉樹的三種遍歷(先序遍歷,中序遍歷,后序遍歷),不是本文關注的重點,請自行查閱相關資料。本文的重點是如何用遞歸和迭代分別實現二叉樹的三種遍歷。
leetcode上有三道題分別求三種遍歷結果:Binary Tree Preorder Traversal 、Binary Tree Inorder Traversal 、 Binary Tree Postorder Traversal
遞歸###
遞歸解法不用多說,只需在遞歸部分的不同位置將節點value置入數組即可:
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
function dfs(root, ans) {
if (!root) return;
// 先序
// ans.push(root.val);
dfs(root.left, ans);
// 中序
// ans.push(root.val);
dfs(root.right, ans);
// 后序
// ans.push(root.val);
}
var preorderTraversal = function(root) {
var ans = [];
dfs(root, ans);
return ans;
};
迭代###
難點是迭代。
如果把二叉樹看做圖,那么二叉樹的遍歷其實就是圖的深度優先遍歷,而圖的深度優先遍歷能用手動模擬棧來解,那么二叉樹的遍歷也是可以的。
用棧模擬圖的深度優先遍歷,每次把父親節點的兒子節點的一個入棧,並刪除該兒子節點
當父親節點的所有兒子節點都被刪除時,父親節點出棧
我們以下面一棵二叉樹舉例:
1
/ \
2 5
/ \
3 4
如何用棧模擬遍歷該二叉樹?我們用 stack
數組模擬棧。
- 將root入棧。此時
stack = [1]
- 遍歷root的子節點,將左子節點入棧。
stack = [1, 2]
- 此時棧頂元素為
2
,開始遍歷它的子節點。左子節點為3
, 入棧。stack = [1, 2, 3]
- 此時棧頂元素為
3
,它沒有子節點,將它出棧。stack = [1, 2]
- 此時棧頂元素為
2
,遍歷它的子節點。左子節點已經被遍歷,於是右子節點,入棧。stack = [1, 2, 4]
- 此時棧頂元素為
4
,和3
一樣它也沒有左右子節點,出棧。stack = [1, 2]
- 此時棧頂元素為
2
, 當父親節點的所有兒子節點都被刪除時,父親節點出棧,於是它出棧。stack = [1]
- 此時棧頂元素為
1
,它的左子節點已經遍歷,遍歷右子節點,將5
入棧。stack = [1, 5]
- 此時棧頂元素為
5
, 它沒有子節點,出棧。stack = [1]
- 此時棧頂元素為
1
, 它的子節點都被遍歷過了,出棧。stack=[]
- 此時stack數組長度為0,迭代結束。
- 先序遍歷
先序遍歷只需按照如上的步驟模擬棧,在每次入棧的時候將節點的value值放入ans數組即可。
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var preorderTraversal = function(root) {
if (!root) return [];
var stack = [] // 棧模擬
, ans = [];
stack.push(root);
ans.push(root.val);
while (stack.length) {
var elem = stack[stack.length - 1];
if (elem.left) {
ans.push(elem.left.val);
stack.push(elem.left);
elem.left = null;
} else if (elem.right) {
ans.push(elem.right.val);
stack.push(elem.right);
elem.right = null;
} else
stack.pop();
}
return ans;
};
還有一種更簡潔的代碼寫法,因為二叉樹父節點最多就兩個子節點,所以直接遍歷兩個節點,然后在棧中刪除父節點即可。
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var preorderTraversal = function(root) {
if (!root) return [];
var stack = [] // 棧模擬
, ans = [];
stack.push(root);
while (stack.length) {
var elem = stack.pop();
ans.push(elem.val);
if (elem.right)
stack.push(elem.right);
if (elem.left)
stack.push(elem.left);
}
return ans;
};
- 后序遍歷
和先序遍歷略有不同的是,先序遍歷是先遍歷父節點,所以父節點的value值要在入棧的時候就放入ans數組,而后序遍歷是最后遍歷父節點,所以當父節點出棧時(此時左右子樹都已經遍歷完畢),把節點的value值放入ans數組即可:
var postorderTraversal = function(root) {
if (!root) return [];
var stack = [] // 棧模擬
, ans = [];
stack.push(root);
while (stack.length) {
var elem = stack[stack.length - 1];
if (elem.left) {
stack.push(elem.left);
elem.left = null;
} else if (elem.right) {
stack.push(elem.right);
elem.right = null;
} else {
var a = stack.pop();
ans.push(a.val);
}
}
return ans;
};
- 中序遍歷
中序遍歷是三大遍歷里最復雜的。先序遍歷是先遍歷父節點,所以節點入棧時存儲value值,后序遍歷是最后遍歷父節點,所以節點出棧時存儲value值,中序遍歷呢?
在leetcode中,中序遍歷這題比先序和后序多了一個tag - hash table,而如何hash也正是本題難點。中序遍歷是在父節點的左子樹遍歷完后,將父節點的value值存入ans數組的,那么如何判斷左子樹已經遍歷完了呢?
比如下面這棵二叉樹:
1
/ \
2 5
/ \
3 4
當遍歷到 2
這個節點時,它有左節點,按照先序和后序遍歷的做法,將左節點入棧,同時將 2
所在節點的left置為null,當節點 3
出棧后,判斷 2
節點的左右子節點,這時發現左節點為null,說明已經遍歷過了,於是 value=2
存入ans數組,然后 4
所在節點入棧,然后 4
再出棧,這時棧頂元素又是2,而這時再次判斷左右子節點,發現左子節點為null,認為左子節點遍歷過了,value=2
再次存入ans數組!看到這里,你或許有點眉目了,我們不能用置為null來表示節點已經遍歷,而應該用正確的hash方式,這里我用 elem.left=1
表示elem的左節點已經被遍歷過了,用 elem.left=0
表示elem節點所在的值已經存入ans數組了。
var inorderTraversal = function(root) {
if (!root) return [];
var stack = [], ans = [];
stack.push(root);
while (stack.length) {
var elem = stack[stack.length - 1];
if (elem.left === 1) {
elem.left = 0;
ans.push(elem.val);
} else if (elem.left) {
stack.push(elem.left);
elem.left = 1;
} else if (elem.left === null) {
elem.left = 1;
} else if (elem.right) {
stack.push(elem.right);
elem.right = null;
} else {
stack.pop();
}
}
return ans;
};