用递归的方法遍历而二叉树,是深度优先搜索的一个基本应用,就像是爬格子之于动态规划,三数之和之于双指针,反转链表之于链表一样。
但是用递归栈来模拟,则是一道medium甚至hard的题目,如果要保证在面试中马上需要反复练习。
首先看一个模拟递归栈的模板:
var traverseWithCallStack(root){
//用来模拟递归栈的数组
const stk = [];
//存一下传进来的root
let node = root;
//遍历二叉树的部分
while(stk.length || node){
//先深度遍历左子树
while(node){
stk.push(node);
node = node.left;
}
//这时候node已经变成undefined了,所以要把node赋值为数组的最后一个
node = stk.pop();
//如果node.right为空,那么node赋值为空,进入下一次外层循环的时候就继续弹栈。
//如果node.right不为空,那么node就赋值为右节点,进入下一次外层循环的时候遍历右子树。
node = node.right;
}
}
在这个模板的基础上,稍作修改,就可以实现三种深度遍历。
前序遍历是最好模拟的,只要在深度遍历左子树的时候保存节点的val
值即可。
var traverseWithCallStack(root){
//用来存储输出结果的数组
const res = [];
//用来模拟递归栈的数组
const stk = [];
//存一下传进来的root
let node = root;
//遍历二叉树的部分
while(stk.length || node){
//先深度遍历左子树
while(node){
stk.push(node);
res.push(node.val);
node = node.left;
}
//这时候node已经变成undefined了,所以要把node赋值为数组的最后一个
node = stk.pop();
//如果node.right为空,那么node赋值为空,进入下一次外层循环的时候就继续弹栈。
//如果node.right不为空,那么node就赋值为右节点,进入下一次外层循环的时候遍历右子树。
node = node.right;
}
return res;
}
当然,前序遍历有一个广度优先的实现方法,更加简单(但是不通用):
val
加入到答案数组中,然后先将栈顶元素的右孩子压入栈中,再将栈顶元素的左孩子压入栈中var preorderTraversal = function(root) {
const ret = []
if(root === null) return ret
const stack = []
stack.push(root)
while(stack.length > 0){
const node = stack.pop()
ret.push(node.val)
if(node.right) {
stack.push(node.right)
}
if(node.left) {
stack.push(node.left)
}
}
return ret
};
中序遍历和前序遍历一样,只是把res.push(node.val)
换了个位置,即在深度遍历完左子树之后保存当前节点的值。
var traverseWithCallStack(root){
//用来存储输出结果的数组
const res = [];
//用来模拟递归栈的数组
const stk = [];
//存一下传进来的root
let node = root;
//遍历二叉树的部分
while(stk.length || node){
//先深度遍历左子树
while(node){
stk.push(node);
node = node.left;
}
//这时候node已经变成undefined了,所以要把node赋值为数组的最后一个
node = stk.pop();
res.push(node.val);
//如果node.right为空,那么node赋值为空,进入下一次外层循环的时候就继续弹栈。
//如果node.right不为空,那么node就赋值为右节点,进入下一次外层循环的时候遍历右子树。
node = node.right;
}
return res;
}
后序遍历比上面两种要复杂,因为它是在弹栈的时候保存当前节点值的,因此需要:
var traverseWithCallStack(root){
//用来存储输出结果的数组
const res = [];
//用来模拟递归栈的数组
const stk = [];
//存一下传进来的root
let node = root;
//这里比模板多加一个变量prev,用来判断上述的第二种情况。
let prev = null;
//遍历二叉树的部分
while(stk.length || node){
//先深度遍历左子树
while(node){
stk.push(node);
node = node.left;
}
//这里先不慌弹栈,先看看栈顶元素有没有右子树
node = stk[stk.length-1];
if(node.right == null || node.right == prev){
//这是弹栈的分支
stk.pop();
res.push(node.val);
//别忘了更新prev
prev = node;
//最后注意这里别忘了把node置空,好在下一轮循环时弹栈
node = null;
}else{
//这是遍历右子树的分支
node = node.right;
}
//这个语句在上面两个分支都有过了,就不要它了
// node = node.right;
}
return res;
}
我们注意到,前序遍历的输出顺序是:“中、左、右”,如果我们将顺序改成:“中,右,左”,那么它就是后序遍历“左,右,中”的reverse
。
在前序遍历的解法中,有一个广度优先的实现方法,这种方法很简单,但是不通用。我们可以稍加修改这个广度优先的解法,先入栈左孩子,再入栈右孩子,这样输出的顺序就是“中,右,左”,再将它反过来,就成了后序遍历的输出。
需要注意的是,这种方法需要额外的O(n)时间来反转一个Array,效率上比较低。它的优点就是代码逻辑很容易理解。
var postorderTraversal = function(root) {
if(!root) return []
const stack = [root]
const outputStack = []
// 先按照"中,右,左"的顺序“前序遍历”
while(stack.length) {
const node = stack.pop()
outputStack.push(node.val)
if(node.left) stack.push(node.left)
if(node.right) stack.push(node.right)
}
return outputStack.reverse()
};