bigywater.tech
算法与数据结构 · LeetCode 106

LeetCode 106. 从中序与后序遍历序列构造二叉树

给定一棵二叉树的中序遍历序列和后序遍历序列(值互不相同),重建出这棵树:postorder 的最后一个元素是根(后序是"左、右、根"),在 inorder 里定位这个根,就能把序列切成左右两半分别递归。一个真实会犯的错误是照搬上一题"前序定根"的思路,误把 postorder[0] 当成根——但后序的根在最后不在最前。本文用 2000 组随机二叉树验证这个错误有多常见,给出正确写法、5 种语言实现,以及 postorder 末尾定根的分步演示。

55%
2,000 组随机树里,误把 postorder[0] 当根后,有 55% 的情况会直接因为下标越界崩溃
4 步
分步演示:postorder 末尾定根,inorder 里定位切分左右子树,递归重复,另附 5 种语言实现

题意与示例

本文只用自己的话概括题意,完整约束以官方页面为准。给定一棵二叉树的中序遍历序列和后序遍历序列(值互不相同),重建出这棵二叉树。一共 5 组用例,覆盖:官方示例、单结点、两结点、三结点的左链和右链:

真实实测 · 本机执行输出
inorder=[9, 3, 15, 20, 7] postorder=[9, 15, 7, 20, 3]  →  [3,9,20,null,null,15,7]
inorder=[-1] postorder=[-1]  →  [-1]
inorder=[2, 1] postorder=[2, 1]  →  [1,2]
inorder=[1, 2, 3] postorder=[3, 2, 1]  →  [1,null,2,null,3]
inorder=[3, 2, 1] postorder=[3, 2, 1]  →  [1,2,null,3]

陷阱:照搬"前序定根"的思路,误把 postorder[0] 当成根

上一题"从前序与中序构造二叉树"的做法是取 preorder 的第一个元素当根。如果照搬这个思路来做本题、直接取 postorder[0] 当根,那就错了——后序遍历是"左、右、根",根在最后一个位置,不是第一个。用官方示例验证:

真实实测 · 本机执行输出
inorder=[9, 3, 15, 20, 7] postorder=[9, 15, 7, 20, 3]\n正确写法(root = postorder[-1])  →  [3,9,20,null,null,15,7]\n陷阱写法(root = postorder[0])   →  程序崩溃:list index out of range

这个反例甚至没能重建出一棵"看起来正常但错误"的树,而是直接崩溃了——因为 postorder[0]=9 在 inorder 里的位置是 0(最左边),按这个位置切分出的"左子树"区间是空的,但后面递归传下去的 postorder 下标范围却没跟着对齐,很快就会越界。为了确认这不是巧合,做了一次随机测试:生成 2,000 组随机二叉树(结点数 1 到 10,值互不相同):

真实实测 · 本机执行输出
2,000 组随机树,陷阱写法(postorder[0] 当根):\n直接崩溃(数组下标越界)   1,099 组,占 55.0%\n没崩溃但重建出错误的树      701 组,占 35.1%\n碰巧和正确结果一致          200 组,占 10.0%(几乎都是结点数很少的简单树)

超过一半直接崩溃,三分之一会静静地重建出错误的树,只有一成的简单情况碰巧对。根本原因是:postorder 的"根在最后"和 preorder 的"根在最前"是两件不能互相替换的事,切分左右子树时对应的下标范围也完全不同,不能照搬。

哈希表定位 + 递归切分:五种语言的实现

先把 inorder 的值到下标的映射建成一张哈希表。递归函数 helper(inL, inR, postL, postR) 每次处理 inorder 的 [inL, inR] 段和 postorder 的 [postL, postR] 段:postorder[postR](最后一个)是这一段的根,在 inorder 里查到根的位置 mid,左子树的结点数是 mid - inL,据此切出 postorder 里对应左右子树的区间,递归重建。时间 O(n),空间 O(n)。Python、Java、Go、JavaScript、C++ 都是真实编译运行的,用同一组 5 个测试用例。

Python · 解法代码def build_tree(inorder, postorder): idx = {v: i for i, v in enumerate(inorder)} def helper(in_l, in_r, post_l, post_r): if in_l > in_r: return None root_val = postorder[post_r] # postorder 的最后一个就是这段的根 root = TreeNode(root_val) mid = idx[root_val] left_size = mid - in_l root.left = helper(in_l, mid - 1, post_l, post_l + left_size - 1) root.right = helper(mid + 1, in_r, post_l + left_size, post_r - 1) return root return helper(0, len(inorder) - 1, 0, len(postorder) - 1)
真实实测 · Python · 前几组用例的输出
$ python3 solution.py
inorder=[9, 3, 15, 20, 7] postorder=[9, 15, 7, 20, 3]  →  [3,9,20,null,null,15,7]
inorder=[-1] postorder=[-1]  →  [-1]
inorder=[2, 1] postorder=[2, 1]  →  [1,2]
inorder=[1, 2, 3] postorder=[3, 2, 1]  →  [1,null,2,null,3]
inorder=[3, 2, 1] postorder=[3, 2, 1]  →  [1,2,null,3]
Java · 解法代码class Solution { public TreeNode buildTree(int[] inorder, int[] postorder) { Map<Integer, Integer> idx = new HashMap<>(); for (int i = 0; i < inorder.length; i++) idx.put(inorder[i], i); return helper(postorder, 0, inorder.length - 1, 0, postorder.length - 1, idx); } private TreeNode helper(int[] postorder, int inL, int inR, int postL, int postR, Map<Integer, Integer> idx) { if (inL > inR) return null; int rootVal = postorder[postR]; // postorder 的最后一个就是这段的根 TreeNode root = new TreeNode(rootVal); int mid = idx.get(rootVal); int leftSize = mid - inL; root.left = helper(postorder, inL, mid - 1, postL, postL + leftSize - 1, idx); root.right = helper(postorder, mid + 1, inR, postL + leftSize, postR - 1, idx); return root; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java && java -cp build Main
inorder=[9, 3, 15, 20, 7] postorder=[9, 15, 7, 20, 3]  →  [3,9,20,null,null,15,7]
inorder=[-1] postorder=[-1]  →  [-1]
inorder=[2, 1] postorder=[2, 1]  →  [1,2]
inorder=[1, 2, 3] postorder=[3, 2, 1]  →  [1,null,2,null,3]
inorder=[3, 2, 1] postorder=[3, 2, 1]  →  [1,2,null,3]
Go · 解法代码func buildTree(inorder []int, postorder []int) *TreeNode { idx := map[int]int{} for i, v := range inorder { idx[v] = i } var helper func(inL, inR, postL, postR int) *TreeNode helper = func(inL, inR, postL, postR int) *TreeNode { if inL > inR { return nil } rootVal := postorder[postR] // postorder 的最后一个就是这段的根 root := &TreeNode{Val: rootVal} mid := idx[rootVal] leftSize := mid - inL root.Left = helper(inL, mid-1, postL, postL+leftSize-1) root.Right = helper(mid+1, inR, postL+leftSize, postR-1) return root } return helper(0, len(inorder)-1, 0, len(postorder)-1) }
真实实测 · Go · 前几组用例的输出
$ go run solution.go
inorder=[9, 3, 15, 20, 7] postorder=[9, 15, 7, 20, 3]  →  [3,9,20,null,null,15,7]
inorder=[-1] postorder=[-1]  →  [-1]
inorder=[2, 1] postorder=[2, 1]  →  [1,2]
inorder=[1, 2, 3] postorder=[3, 2, 1]  →  [1,null,2,null,3]
inorder=[3, 2, 1] postorder=[3, 2, 1]  →  [1,2,null,3]
JavaScript · 解法代码function buildTree(inorder, postorder) { const idx = new Map(); inorder.forEach((v, i) => idx.set(v, i)); function helper(inL, inR, postL, postR) { if (inL > inR) return null; const rootVal = postorder[postR]; // postorder 的最后一个就是这段的根 const root = new TreeNode(rootVal); const mid = idx.get(rootVal); const leftSize = mid - inL; root.left = helper(inL, mid - 1, postL, postL + leftSize - 1); root.right = helper(mid + 1, inR, postL + leftSize, postR - 1); return root; } return helper(0, inorder.length - 1, 0, postorder.length - 1); }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js
inorder=[9, 3, 15, 20, 7] postorder=[9, 15, 7, 20, 3]  →  [3,9,20,null,null,15,7]
inorder=[-1] postorder=[-1]  →  [-1]
inorder=[2, 1] postorder=[2, 1]  →  [1,2]
inorder=[1, 2, 3] postorder=[3, 2, 1]  →  [1,null,2,null,3]
inorder=[3, 2, 1] postorder=[3, 2, 1]  →  [1,2,null,3]
C++ · 解法代码class Solution { public: TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) { unordered_map<int,int> idx; for (int i = 0; i < (int)inorder.size(); i++) idx[inorder[i]] = i; return helper(postorder, 0, inorder.size()-1, 0, postorder.size()-1, idx); } private: TreeNode* helper(vector<int>& postorder, int inL, int inR, int postL, int postR, unordered_map<int,int>& idx) { if (inL > inR) return nullptr; int rootVal = postorder[postR]; // postorder 的最后一个就是这段的根 TreeNode* root = new TreeNode(rootVal); int mid = idx[rootVal]; int leftSize = mid - inL; root->left = helper(postorder, inL, mid-1, postL, postL+leftSize-1, idx); root->right = helper(postorder, mid+1, inR, postL+leftSize, postR-1, idx); return root; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin
inorder=[9, 3, 15, 20, 7] postorder=[9, 15, 7, 20, 3]  →  [3,9,20,null,null,15,7]
inorder=[-1] postorder=[-1]  →  [-1]
inorder=[2, 1] postorder=[2, 1]  →  [1,2]
inorder=[1, 2, 3] postorder=[3, 2, 1]  →  [1,null,2,null,3]
inorder=[3, 2, 1] postorder=[3, 2, 1]  →  [1,2,null,3]
真实实测 · 本机执行输出
共 5 组用例(含边界),以 Python 结果为基准逐组比对:\n  Python      通过 5/5\n  Java        通过 5/5\n  Go          通过 5/5\n  JavaScript  通过 5/5\n  C++         通过 5/5

随机对拍

Python 版本的哈希表实现另外和一个独立写的、每次现查 inorder.index() 的版本做了随机对拍:

真实实测 · 本机执行输出
随机对拍 5,000 组(结点数 0 到 10 的随机二叉树,值互不相同),哈希表版与现查 index 版的结果全部一致,0 处不一致。

另外四种语言只在上面 5 组固定用例上验证过,没有做大规模随机对拍。

分步演示:postorder 末尾定根,inorder 切分左右

下面的演示用官方示例 inorder=[9,3,15,20,7]、postorder=[9,15,7,20,3]:每一步先从 postorder 段的末尾取出根,再去 inorder 段里定位这个根,把左右两侧分别切给左右子树递归处理。蓝色是当前正在处理的区间,绿色是已经确定归属的结点。每一帧都是把上面的解法在这组输入上真实执行记录下来的。

从遍历序列重建二叉树:postorder 末尾定根,inorder 切分左右
当前处理的区间已经确定归属的结点

    参考与说明

    • 官方题目页:LeetCode 106. 从中序与后序遍历序列构造二叉树。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚。
    • 没有覆盖:值有重复时该怎么处理(本题官方保证值互不相同);与上一题(前序+中序)写法的完整对照表。
    京ICP备2026067502号-1京公网安备11010502063487号