二叉树前序、中序、后序非递归算法万能模板


写在前面

之前介绍过面试题:二叉树的非递归前序遍历面试题:二叉树的非递归中序遍历面试题:二叉树的非递归后序遍历,感兴趣的同学可以先去看看那几篇文章。

前序和中序的非递归算法非常相似,包括后序的方法三介绍的算法也很相似。本文将总结这3个相似算法的相同点,总结出一套万能模板,适用于二叉树的前序、中序、后序非递归算法。

万能模板

  • 1.定义一个栈和cur指针
  • 2.将cur及其所有左子孙节点入栈
  • 3.出栈
  • 4.将cur指向它的右孩子,然后从步骤2开始循环

模板代码

// 定义节点
type Node struct {
	Val         int
	Left, Right *Node
}

func TreeVisit(root *Node) []int {
	result := []int{}
	// 1.定义一个栈
	s := newStack()
	cur := root
	for true {
		// 从当前点开始,将它及其所有的左子孙都入栈
		for cur != nil {
			// TODO: 如果是前序遍历,则在此处访问cur节点。因为前序遍历的顺序是:中、左、右,因此需要先访问,然后入栈
			// cur入栈
			s.push(cur)
			cur = cur.Left
		}
		// 如果栈为空,则退出循环
		if s.empty() {
			break
		}
		// 此时当前节点cur为空,弹出栈顶元素
		cur = s.pop()
		// TODO: 如果是中序遍历,则在此处访问cur节点。因为中序遍历的顺序是:左、中、右,此时以cur为根节点的子树,左孩子为空或者已经被访问过了,此时应该访问cur节点了
		// TODO: 如果是后序遍历,则在此处访问cur节点。因为后序遍历的顺序是左、右、中,此时以cur为根节点的子树,左孩子为空或者已经被访问过了。但是能不能访问cur节点
		// 还不确定,还需要判断一下它的右孩子是否已经被访问过,因此需要增加一个pre节点记录上一次访问过的节点,如果pre=cur.Right则表示cur的右孩子已经被访问过,可以访问cur了。
		// 可以确定的是,在后序遍历中,访问完右孩子,紧接着肯定是访问根节点,所以pre一定是等于cur的右孩子,如果不是说明cur的右孩子还没被访问。
		// 指向当前节点的右孩子
		cur = cur.Right
	}

	return result
}

上面模板代码中,在关键地方列出了TODO,下面利用万能模板分别实现前序、中序、后序算法

前序

// 二叉树的非递归前序遍历
func TreeBeforeVisit(root *Node) []int {
	result := []int{}
	// 1.定义一个栈
	s := newStack()
	cur := root
	for true {
		// 从当前点开始,将它及其所有的左子孙都入栈
		for cur != nil {
			// 在此处访问cur节点
			result = append(result, cur.Val)
			// cur入栈
			s.push(cur)
			cur = cur.Left
		}
		// 如果栈为空,则退出循环
		if s.empty() {
			break
		}
		// 此时当前节点cur为空,弹出栈顶元素
		cur = s.pop()
		cur = cur.Right
	}

	return result
}

中序

// 二叉树的非递归中序遍历
func TreeMidVisit(root *Node) []int {
	result := []int{}
	// 1.定义一个栈
	s := newStack()
	cur := root
	for true {
		// 从当前点开始,将它及其所有的左子孙都入栈
		for cur != nil {
			// cur入栈
			s.push(cur)
			cur = cur.Left
		}
		// 如果栈为空,则退出循环
		if s.empty() {
			break
		}
		// 此时当前节点cur为空,弹出栈顶元素
		cur = s.pop()
		result = append(result, cur.Val)
		cur = cur.Right
	}

	return result
}

后序

// 二叉树的非递归中序遍历
func TreeAfterVisit(root *Node) []int {
	result := []int{}
	// 1.定义一个栈
	s := newStack()
	cur := root
	var pre *Node
	for true {
		// 从当前点开始,将它及其所有的左子孙都入栈
		for cur != nil {
			// cur入栈
			s.push(cur)
			cur = cur.Left
		}
		// 如果栈为空,则退出循环
		if s.empty() {
			break
		}
		// 此时当前节点cur为空,弹出栈顶元素
		cur = s.pop()
        // 只有当cur的右孩子为空或者cur的右孩子已经访问过了,才能访问cur,所以需要用pre记录上一次访问到的节点
		if cur.Right == nil || cur.Right == pre {
			// 访问cur节点
			result = append(result, cur.Val)
			pre = cur
			cur = nil // 这步很重要。逻辑走到这里说明以p为根的子树已经访问完了,下一步需要访问p的父节点,需要从栈栈弹出一个元素,所以这里将p设置为空
		} else {
            // 否则访问p的右孩子
			s.push(cur) // 这一步很重要,需要将p重新放回栈,p不能被丢弃,后面还要访问p
			cur = cur.Right
		}
	}

	return result
}

后序的算法要复杂一点,需要用pre指针记录一下上一次访问到的节点,并且如果不能访问cur节点,还需要放回栈。

总结

本文介绍了一种万能模板,能够实现二叉树的前序、中序、后序非递归算法,在面试中会经常被问到,希望对读者右用处。

参考

[1]leetcode 145. 二叉树的后序遍历的非递归实现

[2]面试题:二叉树的非递归前序遍历

[3]面试题:二叉树的非递归中序遍历

[4]面试题:二叉树的非递归后序遍历



文章作者: Alex
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Alex !
  目录