题目描述

✅ 889. 从前序与后序遍历序列构造二叉树

image-20260928235853165

image-20260928235853166

题意分析

给出同一棵二叉树的前序、后序遍历,节点值互不相同,构造任意一棵能产生这两份遍历结果的树。题目保证输入有效,不要求还原唯一树形。

前序顺序是根、左子树、右子树,后序顺序是左子树、右子树、根。缺少中序时,某个节点的唯一孩子究竟在左边还是右边可能无法判断;把这种唯一孩子统一放左边,也能得到合法答案。

解法:递归切分遍历区间

核心思路

[!blue]

用 build(preLeft, postLeft, size) 构造同一棵子树:它在前序中从 preLeft 开始,在后序中从 postLeft 开始,两段都包含 size 个节点。前序第一项就是当前根,后序最后一项也对应这个根。

当 size > 1,前序的下一项是第一个被访问的孩子子树的根。若当前根有两个孩子,它必然是左子树根;若只有一个孩子,也统一把这棵子树作为左子树。因此可以用 preorder[preLeft + 1] 定位待构造的左子树。

在后序遍历中,这棵左子树的全部节点连续出现在当前区间开头,而左子树根排在它们最后。找到该根的后序下标 index 后,左子树大小就是闭区间长度 index - postLeft + 1。节点值互异,预先建立后序值到下标的映射,就能直接定位,不必每层重新扫描。

左子树的前序从当前根之后开始,后序仍从 postLeft 开始;右子树的两份遍历都要跳过整个左子树。于是右侧前序起点为 preLeft + 1 + leftSize,后序起点为 postLeft + leftSize,长度为 size - 1 - leftSize,减去的额外一个节点就是当前根。

如果 leftSize == size - 1,说明第一个孩子已经占据全部剩余节点,右子树为空。把唯一孩子放左边不会改变前序或后序顺序,因此仍符合输入。递归中 size == 0 返回空,size == 1 创建根后直接返回,避免再读取不存在的下一项。

每层都按同一组节点切出左右两段,再递归连接到根,两份遍历的对应关系就被逐层保持,最终得到一棵符合要求的树。

解题步骤

  1. 建立后序遍历中“节点值 → 下标”的映射。
  2. 从两个起点均为 0、长度为节点总数开始构造。
  3. 空区间返回空;否则以前序首项创建根,若只有一个节点则直接返回。
  4. 查找前序下一项在后序中的位置,计算 leftSize = index - postLeft + 1。
  5. 左侧从 (preLeft + 1, postLeft) 开始构造 leftSize 个节点。
  6. 右侧从 (preLeft + 1 + leftSize, postLeft + leftSize) 开始,构造剩余的 size - 1 - leftSize 个节点,再返回根。

代码实现

class Solution {
    private int[] preorder;
    private Map<Integer, Integer> postIndex;

    public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {
        this.preorder = preorder;
        postIndex = new HashMap<>();

        for (int i = 0; i < postorder.length; i++) {
            postIndex.put(postorder[i], i);
        }

        return build(0, 0, preorder.length);
    }

    // 两段遍历从各自起点取同样数量,描述一棵子树
    private TreeNode build(int preLeft, int postLeft, int size) {
        if (size == 0) {
            return null;
        }

        TreeNode root = new TreeNode(preorder[preLeft]);

        // 单节点无需再读前序下一项
        if (size == 1) {
            return root;
        }

        int leftRootIndex = postIndex.get(preorder[preLeft + 1]);
        // 后序中的闭区间长度确定左子树规模
        int leftSize = leftRootIndex - postLeft + 1;

        root.left = build(preLeft + 1, postLeft, leftSize);
        // 右侧长度扣除当前根与整棵左子树
        root.right = build(preLeft + 1 + leftSize, postLeft + leftSize, size - 1 - leftSize);

        return root;
    }
}
func constructFromPrePost(preorder []int, postorder []int) *TreeNode {
    postIndex := make(map[int]int, len(postorder))
    for i, value := range postorder {
        postIndex[value] = i
    }

    var build func(preLeft, postLeft, size int) *TreeNode
    // 两段遍历从各自起点取同样数量,描述一棵子树
    build = func(preLeft, postLeft, size int) *TreeNode {
        if size == 0 {
            return nil
        }

        root := &TreeNode{Val: preorder[preLeft]}
        // 单节点无需再读前序下一项
        if size == 1 {
            return root
        }

        leftRootIndex := postIndex[preorder[preLeft+1]]
        // 后序中的闭区间长度确定左子树规模
        leftSize := leftRootIndex - postLeft + 1
        root.Left = build(preLeft+1, postLeft, leftSize)
        // 右侧长度扣除当前根与整棵左子树
        root.Right = build(
            preLeft+1+leftSize,
            postLeft+leftSize,
            size-1-leftSize,
        )
        return root
    }

    return build(0, 0, len(preorder))
}

复杂度分析

  • 时间复杂度:期望 $O(n)$,建表与构造各处理节点一次。
  • 空间复杂度:$O(n)$,索引表加递归栈,不计结果树。

关键点总结

[!green]

  • 两段描述同一棵子树:前后序各自起点不同,但节点数量必须一致。
  • 后序的子树根决定区间末端:它的位置连同当前后序起点确定左子树大小。
  • 单孩子方向不必唯一还原:统一放左侧只是选取一种符合遍历结果的树形。
  • 使用下标避免反复复制:递归传入起点和长度,索引表负责常数时间定位。

易错点总结

[!yellow]

  • 左子树长度漏加一:后序区间包含左子树根本身,应按闭区间计算。
  • 右侧长度只减左子树:还要扣除当前根,才能让两侧与根的节点总数恰好等于 size。
  • 单节点仍读取前序下一项:可能越界或误读其他子树,应先处理叶子边界。
  • 右侧两个起点都加一:前序需要跳过当前根,后序的根位于末尾,所以后序起点只跳过左子树。
  • 要求唯一孩子必须还原到原方向:仅凭前后序无法确定这一点,返回任意合法结果即可。

相似题目

题目 难度 关联与区别
105. 从前序与中序遍历序列构造二叉树 中等 有中序时左右子树划分更直接且可唯一还原,本题只给前序和后序,单孩子方向可能不唯一。
106. 从中序与后序遍历序列构造二叉树 中等 同样由遍历序列恢复树,本题缺少中序,需按前序的下一根在后序中的位置划分。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2024/58828253
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!