当前:LC145 · 二叉树的后序遍历 · 首次出现于 Day 18 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC145 · Binary Tree Postorder Traversal · 树

二叉树的后序遍历:根最后

根一定晚于自己的全部子孙。迭代先走「根、右、左」,整段反转就是「左、右、根」。

后序 = 左子树 → 右子树 → 根。递归按这三行直译。迭代技巧:先做「根、右、左」的类前序(压栈先左后右,右先出栈),最后把结果反转,即得「左、右、根」。时间 O(n),空间 O(h)。

时间 O(n)空间 O(h)结论先行 · 全文约 6 节
导读

这是 LeetCode 145. Binary Tree Postorder Traversal。大白话:给你一棵二叉树的根节点,按「左子树、右子树、根」的顺序把节点值收进数组返回。后序的第一条规律:整棵树的根永远是序列的最后一个元素。

本页先走那棵只有右脊的树:根 1,右孩子 2,2 的左孩子 3。后序要先走完 1 的左子树(空),再走完右子树的后序 [3, 2],最后才写 1,序列是 [3, 2, 1]。3 是叶子,左右都空,最先被写下;1 等两棵子树都结束,落在最后。

后序是「先子树后自身」,因此天然适合需要子树信息汇总的题目,比如树高、直径:必须先知道左右孩子的答案,再决定当前节点。迭代比前序别扭,因为栈天然容易先碰到根。缺的是一种让根排到最后的办法:先按「根、右、左」走一遍,再把整段结果倒过来。

后序的契约:根最后

每个节点都要做三件事:处理自己、走完左子树、走完右子树。后序把处理放在最后,所以根一定晚于自己的全部子孙。对节点 x,先对左孩子做后序,再对右孩子做后序,最后才写 x.Val。空节点直接返回。这不是另一套算法,只是把写入那一行挪到了函数体末尾。

主例 [1, null, 2, 3] 用手走一遍。1 先往左,左空。进入右子树 2,2 先进入左孩子 3。3 的左右都空,写下 3。回到 2,2 的右空,写下 2。回到 1,左右都结束,写下 1。序列是 [3, 2, 1]。2 一定在 3 之后,1 一定在 2 之后。

自底向上的含义就是这句:一个节点被写下时,它的整棵左子树、整棵右子树都已经在答案里了。算高度时,左右递归返回之后才能取 max + 1,框架正好是后序。空树返回空数组;单节点只写下自己,它既是最先也是最后。

自底向上需要先知道子树信息再决定当前节点的题,后序是自然的框架。根最后被访问,不是口号,是「左右都交卷了」的时刻。
左 → 右 → 根
123
结果5
左空,右子树的最左 3

迭代:类前序 + 反转

前序是「根、左、右」。若把入栈顺序对调——弹出后先压左、再压右——右孩子后进先出,得到的访问顺序是「根、右、左」。把「根、右、左」整段反转,左边的根跑到末尾,右边的左右对调回来,恰好是「左、右、根」,也就是后序。

这不是投机。后序「左、右、根」正好是「根、右、左」的镜像:第一个元素变成最后一个,左右的相对位置在反转后回到先左后右。所以迭代版 = 修改版前序遍历 + reverse 结果。和前序共用同一套弹栈手法,只是左右对调,最后再倒一次。

用 [1, 2, 3] 走一遍。类前序弹出立刻写,先压左 2、再压右 3,于是先写出 1,再写出 3,再写出 2,得到 [1, 3, 2]。整段反转变成 [2, 3, 1]。核对:2 是左子树、3 是右子树、1 是根,正是后序。主例那棵右脊树用同一手法会得到 [1, 2, 3] 再反转成 [3, 2, 1]。

也可以额外记住上一个完成的孩子,避免根被提前弹出,那是另一份更长的迭代。本页停在反转技巧:它把「根必须最后」收成一次数组倒序,不必在弹栈时判断左右孩子谁完成了。每个节点仍只进出栈一次,最后再线性反转,总时间 O(n)。

根右左 → 反转 → 左右根
123
结果132
类前序(根右左):1,3,2

Go:递归与迭代反转

solution.goGo
func postorderTraversal(root *TreeNode) []int {
res := []int{}
var dfs func(*TreeNode)
dfs = func(n *TreeNode) {
if n == nil { return }
dfs(n.Left)
dfs(n.Right)
res = append(res, n.Val)
}
dfs(root)
return res
}
// 迭代:类前序 + 反转
func postorderIter(root *TreeNode) []int {
res, stack := []int{}, []*TreeNode{root}
for len(stack) > 0 {
n := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if n == nil { continue }
res = append(res, n.Val)
stack = append(stack, n.Left, n.Right)
}
slices.Reverse(res)
return res
}

1递归:先走完左、再走完右,最后写入根。写入那一行在最后,就是后序。

2迭代弹出立刻写入,访问顺序先变成根、右、左——因为 append 先压左再压右,右后进先出。

3这和前序迭代只差左右入栈对调。前序是先右后左得到根左右;这里先左后右得到根右左。

4slices.Reverse 把「根右左」镜像成「左右根」。最后一个元素必是整棵树的根。

总结

后序根最后;迭代先走根右左,反转一次就是左右根。

  • 一个节点被写下时,左右子树都已经在答案里。自底向上的题(树高、直径)天然走后序框架。
  • 「根、右、左」是「左、右、根」的镜像。和前序共用弹栈手法,只对调左右,再 reverse。
  • 后序序列的最后一个元素必是根,这是从后序重建二叉树时取根的依据。
同族题目
LC144二叉树的前序遍历LC94二叉树的中序遍历LC106从中序与后序遍历序列构造二叉树