写在前面
前面分别介绍了面试题:二叉树的非递归前序遍历和面试题:二叉树的非递归中序遍历,举一反三,本文介绍二叉树的非递归后序遍历
方法一
作者觉得这是最简单的一种方法,就是用两个栈。
申请两个栈s1, s2。后序遍历的步骤如下:
- 申请一个栈记为s1, 然后初始将根节点进行入栈s1
- 从s1中弹出结点记做为cur, 然后将cur的不为空的左右孩子进行入栈s1。将弹出的cur进行入栈s2。
- 不断重复上面的步骤1和步骤2,直到s1为空
- 从s2中弹出元素并打印,打印序列就是后序遍历的顺序。
第二步依次将左右孩子入栈,后面出栈的顺序是右左,所以栈s2中的顺序是中右左,最后出栈就将顺序反过来了,结果是左右中。就是后序遍历了。
下面看具体代码
func TreeAfterVisit(root *Node) []int {
if root == nil {
return nil
}
// 1.定义一个栈
s := newStack()
// 2.定义一个栈
s1 := newStack()
// 根节点进栈
s.push(root)
//var pre *Node
for true {
// 第一个节点出栈
if s.empty() {
break
}
p := s.pop()
// p进入s1
s1.push(p)
// 将栈顶元素的左右孩子入栈
if p.Left != nil {
s.push(p.Left)
}
if p.Right != nil {
s.push(p.Right)
}
}
result := []int{}
for !s1.empty() {
result = append(result, s1.pop().Val)
}
return result
}
方法二
后序遍历是左右中,而前序遍历是中左右,能不能将前序遍历的顺序改一下,改成中右左,然后再逆序,结果就是左右中?
方法二就是这个思路,主要代码如下
func TreeAfterVisit2(root *Node) []int {
result := []int{}
// 1.定义一个栈
s := newStack()
p := root
for true {
for p != nil {
// 访问栈顶元素
result = append(result, p.Val)
// p入栈
s.push(p)
p = p.Right // 注意这里指向右孩子,所有右孩子入栈
}
// 从栈顶出战
if s.empty() {
break
}
p = s.pop()
// 指向左孩子
p = p.Left
}
return reverse(result)
}
func reverse(data []int) []int {
n := len(data)
for i := 0; i < n/2; i++ {
data[i], data[n-1-i] = data[n-1-i], data[i]
}
return data
}
方法二使用的是一个栈,其实方法一夜可以使用一个栈,栈s2的左右就是为了做个逆序操作。
方法三
方法三有点难以理解,先上代码,后面再分析逻辑
func TreeAfterVisit3(root *Node) []int {
result := []int{}
// 1.定义一个栈
s := newStack()
p := root
var pre *Node // 用于记录上一次访问到的节点
for true {
for p != nil {
// p入栈
s.push(p)
p = p.Left
}
// 从栈顶出战
if s.empty() {
break
}
p = s.pop()
// 此时所有左孩子及其左子孙都入栈了,p是最底层的左孩子,或者说p是最底层的根节点。什么时候开始访问p呢?
// 只有当p的右孩子为空或者p的右孩子已经访问过了,才能访问p,所以需要用pre记录上一次访问到的节点
if p.Right == nil || p.Right == pre {
// 访问p
result = append(result, p.Val)
pre = p
p = nil // 这步很重要。逻辑走到这里说明以p为根的子树已经访问完了,下一步需要访问p的父节点,需要从栈栈弹出一个元素,所以这里将p设置为空
} else {
// 否则访问p的右孩子
s.push(p) // 这一步很重要,需要将p重新放回栈,p不能被丢弃,后面还要访问p
p = p.Right
}
}
return result
}
这个算法的巧妙之处在于,记录了一下上一次访问过的节点pre,当要访问当前节点的时候,只有当前节点的右孩子为空,或者右孩子已经被访问过,才能访问当前节点。
参考
[1]leetcode 145. 二叉树的后序遍历的非递归实现