栈模拟遍历二叉树前、中、后序模板

栈模拟遍历二叉树前、中、后序模板

    • 前序遍历
      • 骚操作
    • 中序遍历
    • 后序遍历
      • 骚操作

递归的方法遍历而二叉树,是深度优先搜索的一个基本应用,就像是爬格子之于动态规划,三数之和之于双指针,反转链表之于链表一样。
但是用递归栈来模拟,则是一道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;
}

骚操作

当然,前序遍历有一个广度优先的实现方法,更加简单(但是不通用):

  1. 首先将root元素压入栈中。
  2. 每次循环中,先弹出栈顶元素,将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;
}

后序遍历

后序遍历比上面两种要复杂,因为它是在弹栈的时候保存当前节点值的,因此需要:

  1. 判断当前节点的右子树是不是空的,如果是空的,直接弹栈并保存当前节点值;
  2. 如果不是空的暂时不能把当前节点弹出,而是继续遍历右子树的节点压栈,当弹栈弹回到这个节点的时候,注意要直接弹出该节点而不是又遍历这个节点的右子树(以免陷入死循环)。
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()
};

你可能感兴趣的:(小白的数据结构与算法笔记,二叉树,数据结构,算法,栈)