题目描述

✅ 145. 二叉树的后序遍历

image-20260928201957509

image-20260928201957510

image-20260928201957511

题意分析

按后序遍历输出二叉树的节点值:对每一棵子树,都先输出左子树的后序结果,再输出右子树的后序结果,最后输出这棵子树的根节点。

顺序要求作用于每个节点,不是只把整棵树分成三个简单位置。左右孩子为空时跳过对应部分,空树返回空列表;只读取结构,不修改节点连接。

解法:递归后序遍历

核心思路

[!blue]

递归直接对应后序定义。处理一棵树时,先让递归函数完成左子树,再完成右子树,最后把当前根节点的值加入结果。

递归遇到空节点就返回,表示这部分没有需要输出的值。非空节点的两个孩子都处理完成后,结果中已经按顺序包含了左、右子树的完整后序序列,再追加当前根,就得到当前整棵子树的后序结果。

所有递归层共用同一个结果列表,按遍历顺序尾部追加,不需要每层创建列表再合并。调用栈负责记住“子树返回后,还要处理另一侧或输出父节点”的位置。

解题步骤

  1. 创建结果列表,从根节点开始递归。
  2. 当前节点为空时直接返回。
  3. 递归左孩子,再递归右孩子。
  4. 两侧完成后追加当前节点的值,最外层递归结束后返回结果列表。

代码实现

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        traverse(root, result);
        return result;
    }

    private void traverse(TreeNode node, List<Integer> result) {
        if (node == null) {
            return;
        }

        traverse(node.left, result);
        traverse(node.right, result);
        result.add(node.val);
    }
}
func postorderTraversal(root *TreeNode) []int {
    result := make([]int, 0)
    var traverse func(*TreeNode)
    traverse = func(node *TreeNode) {
        if node == nil {
            return
        }

        traverse(node.Left)
        traverse(node.Right)
        result = append(result, node.Val)
    }

    traverse(root)
    return result
}

复杂度分析

  • 时间复杂度:$O(n)$,每个节点访问一次并追加一次。
  • 空间复杂度:$O(h)$,调用栈深度等于树高;不计返回的结果列表,最坏为 $O(n)$。

关键点总结

[!green]

  • 输出节点的时机决定遍历类型,后序就是在左右子树都完成之后输出。
  • 空节点负责终止递归,父节点依靠调用栈在孩子处理完后继续执行。
  • 共享结果列表并尾部追加,避免重复创建和拼接各子树的列表。

解法:根右左遍历后反转结果

核心思路

[!blue]

显式栈很容易实现“弹出节点就记录”的遍历,但后序要求父节点最后输出。这里先改变收集顺序:得到根、右子树、左子树的镜像前序,再把整个结果反转,就能恢复左、右、根。

这个关系不仅对三个节点成立。镜像前序可以写成“根 + 右子树的镜像前序 + 左子树的镜像前序”;整体反转后,三块顺序变成“左子树块 + 右子树块 + 根”,同时每个子树块内部也被反转。对子树重复同样的推理,内部恰好也变成后序,因此整个序列是正确的后序结果。

为了让右子树先处理,弹出一个节点并记录后,先压左孩子,再压右孩子。栈后进先出,右孩子及其后代会先被处理完,之后才轮到左子树。

所有节点收集结束后只做一次整体反转。结果采用尾部追加,避免每次向数组头部插入造成反复搬移;遍历与反转都保持线性时间。

解题步骤

  1. 创建空结果列表,若根为空则直接返回。
  2. 将根压栈。栈非空时弹出一个节点,将它的值追加到结果末尾。
  3. 先压入非空左孩子,再压入非空右孩子,使实际访问顺序为根、右、左。
  4. 重复直到栈为空,此时已经收集全部节点。
  5. 将结果列表整体反转,返回左、右、根的后序顺序。

代码实现

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();

        if (root == null) {
            return res;
        }

        Deque<TreeNode> stack = new ArrayDeque<>();

        stack.push(root);

        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();

            res.add(node.val);

            if (node.left != null) {
                stack.push(node.left);
            }

            if (node.right != null) {
                stack.push(node.right);
            }
        }

        // 当前顺序是根右左,反转后得到左右根。
        Collections.reverse(res);

        return res;
    }
}
func postorderTraversal(root *TreeNode) []int {
    res := make([]int, 0)
    if root == nil {
        return res
    }

    stack := []*TreeNode{
        root,
    }
    for len(stack) > 0 {
        node := stack[len(stack)-1]
        stack = stack[:len(stack)-1]
        res = append(res, node.Val)
        if node.Left != nil {
            stack = append(stack, node.Left)
        }
        if node.Right != nil {
            stack = append(stack, node.Right)
        }
    }

    // 根右左反转后就是后序的左右根。
    for left, right := 0, len(res)-1; left < right; left, right = left+1, right-1 {
        res[left], res[right] = res[right], res[left]
    }
    return res
}

复杂度分析

  • 时间复杂度:$O(n)$。每个节点入栈、出栈一次,最后反转结果仍是线性操作。
  • 空间复杂度:$O(h)$,h 为树高,最坏为 $O(n)$;返回结果不计入额外空间。

关键点总结

[!green]

  • 根右左的整体反转,会同时交换子树块顺序与块内顺序,递归地得到左右根。
  • 压栈顺序与实际访问顺序相反,先压左再压右才能先访问右子树。
  • 一次末尾反转代替多次头部插入,避免数组结果退化成二次时间。

易错点总结

[!yellow]

  • 递归中在两个孩子之前或之间记录节点,分别会变成前序或中序;后序必须在两次递归之后记录。
  • 迭代中沿用先右后左压栈,会先得到根左右,反转后左右子树的顺序仍然错误。
  • 忘记最后整体反转,返回的只是收集中间结果,不是后序。
  • 将空节点压栈后不判断就读取值,会触发空指针错误,应只压入真实孩子。
  • 对数组列表反复向下标 0 插入,会不断搬移已有元素,最坏需要二次时间。
  • 使用双端队列时混用队尾插入和队头弹出,会变成队列;这里需要始终遵循栈的后进先出。

相似题目

题目 难度 关联与区别
94. 二叉树的中序遍历 简单 同样用显式栈保存尚未完成的访问,本题节点需等左右子树都结束后才能输出。
104. 二叉树的最大深度 简单 求高度依赖孩子结果,后序遍历正好提供自底向上的计算顺序。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/87975351
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!