LeetCode 106. 从中序与后序遍历序列构造二叉树
问题描述
给定两个整数数组 inorder 和 postorder,其中 inorder 是二叉树的中序遍历,postorder 是同一棵树的后序遍历,请构造并返回这棵二叉树。
假设树中没有重复的元素。
示例:
输入: inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]
输出: [3,9,20,null,null,15,7]
解释:
中序遍历: 9 → 3 → 15 → 20 → 7 (左 → 根 → 右)
后序遍历: 9 → 15 → 7 → 20 → 3 (左 → 右 → 根)
算法思路
核心:
- 后序遍历特点:最后一个元素总是当前子树的根节点
- 中序遍历特点:根节点将数组分为左子树和右子树两部分
递归构造过程:
- 从后序遍历的末尾获取根节点值
- 在中序遍历中找到根节点位置,分割左右子树
- 递归构造右子树
- 递归构造左子树
- 返回构造的根节点
关键:
- 处理顺序:
- 必须先构造右子树,再构造左子树
- 因为后序遍历顺序是 [左子树, 右子树, 根]
- 从后往前遍历时,先遇到右子树的根,再遇到左子树的根
- 索引管理:使用全局索引或引用跟踪后序遍历的当前位置
- 哈希优化:用HashMap存储中序遍历的值到索引的映射,避免每次线性搜索
代码实现
方法一:递归 + HashMap优化
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
private int postIndex; // 后序遍历的当前索引(从后往前)
private Map<Integer, Integer> inorderMap; // 中序遍历值到索引的映射
/**
* 从中序和后序遍历序列构造二叉树
*
* @param inorder 中序遍历序列
* @param postorder 后序遍历序列
* @return 构造的二叉树根节点
*/
public TreeNode buildTree(int[] inorder, int[] postorder) {
// 初始化后序遍历索引(指向最后一个元素)
postIndex = postorder.length - 1;
// 构建中序遍历的值到索引的映射,优化查找效率
inorderMap = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
inorderMap.put(inorder[i], i);
}
// 递归构造二叉树
return buildTreeHelper(inorder, postorder, 0, inorder.length - 1);
}
/**
* 递归构造指定范围内的子树
*
* @param inorder 中序遍历数组
* @param postorder 后序遍历数组
* @param inStart 中序遍历的起始索引
* @param inEnd 中序遍历的结束索引
* @return 构造的子树根节点
*/
private TreeNode buildTreeHelper(int[] inorder, int[] postorder,
int inStart, int inEnd) {
// 递归终止条件:无效区间
if (inStart > inEnd) {
return null;
}
// 获取当前子树的根节点值(后序遍历的当前元素)
int rootVal = postorder[postIndex];
postIndex--; // 移动到下一个根节点
// 创建根节点
TreeNode root = new TreeNode(rootVal);
// 在中序遍历中找到根节点的位置
int inRootIndex = inorderMap.get(rootVal);
// 重要:先构造右子树,再构造左子树!
// 因为后序遍历顺序是 [左, 右, 根],从后往前遍历时先遇到右子树的根
root.right = buildTreeHelper(inorder, postorder,
inRootIndex + 1, inEnd);
root.left = buildTreeHelper(inorder, postorder,
inStart, inRootIndex - 1);
return root;
}
}
方法二:递归 + 线性搜索
class Solution {
private int postIndex;
/**
* 从中序和后序遍历序列构造二叉树 - 无HashMap优化
*
* @param inorder 中序遍历序列
* @param postorder 后序遍历序列
* @return 构造的二叉树根节点
*/
public TreeNode buildTree(int[] inorder, int[] postorder) {
postIndex = postorder.length - 1;
return buildTreeHelper(inorder, postorder, 0, inorder.length - 1);
}
private TreeNode buildTreeHelper(int[] inorder, int[] postorder,
int inStart, int inEnd) {
if (inStart > inEnd) {
return null;
}
int rootVal = postorder[postIndex--];
TreeNode root = new TreeNode(rootVal);
// 线性搜索根节点在中序遍历中的位置
int inRootIndex = 0;
for (int i = inStart; i <= inEnd; i++) {
if (inorder[i] == rootVal) {
inRootIndex = i;
break;
}
}
// 先右后左的构造顺序
root.right = buildTreeHelper(inorder, postorder, inRootIndex + 1, inEnd);
root.left = buildTreeHelper(inorder, postorder, inStart, inRootIndex - 1);
return root;
}
}
方法三:迭代法(使用栈)
import java.util.Stack;
class Solution {
/**
* 从中序和后序遍历序列构造二叉树 - 迭代实现
*
* @param inorder 中序遍历序列
* @param postorder 后序遍历序列
* @return 构造的二叉树根节点
*/
public TreeNode buildTree(int[] inorder, int[] postorder) {
if (postorder.length == 0) return null;
// 创建根节点
TreeNode root = new TreeNode(postorder[postorder.length - 1]);
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
int inIndex = inorder.length - 1; // 中序遍历从后往前
// 后序遍历从倒数第二个元素开始(最后一个已作为根节点)
for (int i = postorder.length - 2; i >= 0; i--) {
TreeNode node = new TreeNode(postorder[i]);
// 如果栈顶节点不等于当前中序遍历的节点
// 说明当前节点是栈顶节点的右子节点
if (stack.peek().val != inorder[inIndex]) {
stack.peek().right = node;
} else {
// 找到需要挂载左子节点的位置
TreeNode parent = null;
while (!stack.isEmpty() && stack.peek().val == inorder[inIndex]) {
parent = stack.pop();
inIndex--;
}
parent.left = node;
}
stack.push(node);
}
return root;
}
}
算法分析
- 时间复杂度:
- 方法一:O(n) - 每个节点访问一次,HashMap查找O(1)
- 方法二:O(n²) - 每次线性搜索根节点位置
- 方法三:O(n) - 每个节点入栈出栈各一次
- 空间复杂度:
- 方法一:O(n) - HashMap存储 + 递归栈空间
- 方法二:O(n) - 递归栈空间
- 方法三:O(n) - 显式栈空间
- 方法对比:
- 方法一:最优解,时间空间效率都很好
- 方法二:易于理解,但效率较低
- 方法三:避免递归,但逻辑复杂
算法过程
输入:inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]
构造过程:
-
初始状态:
postIndex = 4,postorder[4] = 3- 根节点 = 3
- 中序中3的位置 = 1
- 左子树范围:[0,0] → [9]
- 右子树范围:[2,4] → [15,20,7]
-
构造右子树(
postIndex = 3):- 根节点 = 20(
postorder[3]) - 中序中20的位置 = 3
- 左子树范围:[2,2] → [15]
- 右子树范围:[4,4] → [7]
- 根节点 = 20(
-
构造右子树的右子树(
postIndex = 2):- 根节点 = 7(
postorder[2]) - 中序中7的位置 = 4
- 左右子树都为空
- 根节点 = 7(
-
构造右子树的左子树(
postIndex = 1):- 根节点 = 15(
postorder[1]) - 中序中15的位置 = 2
- 左右子树都为空
- 根节点 = 15(
-
构造左子树(
postIndex = 0):- 根节点 = 9(
postorder[0]) - 中序中9的位置 = 0
- 左右子树都为空
- 根节点 = 9(
最终树结构:
3
/ \
9 20
/ \
15 7
测试用例
public class BuildTreeTest {
static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
// 中序遍历验证
static List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
inorderHelper(root, result);
return result;
}
static void inorderHelper(TreeNode node, List<Integer> result) {
if (node == null) return;
inorderHelper(node.left, result);
result.add(node.val);
inorderHelper(node.right, result);
}
// 后序遍历验证
static List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
postorderHelper(root, result);
return result;
}
static void postorderHelper(TreeNode node, List<Integer> result) {
if (node == null) return;
postorderHelper(node.left, result);
postorderHelper(node.right, result);
result.add(node.val);
}
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
int[] inorder1 = {9,3,15,20,7};
int[] postorder1 = {9,15,7,20,3};
TreeNode root1 = solution.buildTree(inorder1, postorder1);
System.out.println("Test 1 inorder: " + inorderTraversal(root1)); // [9,3,15,20,7]
System.out.println("Test 1 postorder: " + postorderTraversal(root1)); // [9,15,7,20,3]
// 测试用例2:只有左子树
int[] inorder2 = {3,2,1};
int[] postorder2 = {3,2,1};
TreeNode root2 = solution.buildTree(inorder2, postorder2);
System.out.println("Test 2 inorder: " + inorderTraversal(root2)); // [3,2,1]
System.out.println("Test 2 postorder: " + postorderTraversal(root2)); // [3,2,1]
// 测试用例3:只有右子树
int[] inorder3 = {1,2,3};
int[] postorder3 = {3,2,1};
TreeNode root3 = solution.buildTree(inorder3, postorder3);
System.out.println("Test 3 inorder: " + inorderTraversal(root3)); // [1,2,3]
System.out.println("Test 3 postorder: " + postorderTraversal(root3)); // [3,2,1]
// 测试用例4:单节点
int[] inorder4 = {1};
int[] postorder4 = {1};
TreeNode root4 = solution.buildTree(inorder4, postorder4);
System.out.println("Test 4 inorder: " + inorderTraversal(root4)); // [1]
System.out.println("Test 4 postorder: " + postorderTraversal(root4)); // [1]
// 测试用例5:空树
TreeNode root5 = solution.buildTree(new int[]{}, new int[]{});
System.out.println("Test 5: " + (root5 == null ? "null" : "not null")); // null
// 测试用例6:完全二叉树
int[] inorder6 = {4,2,5,1,6,3,7};
int[] postorder6 = {4,5,2,6,7,3,1};
TreeNode root6 = solution.buildTree(inorder6, postorder6);
System.out.println("Test 6 inorder: " + inorderTraversal(root6)); // [4,2,5,1,6,3,7]
System.out.println("Test 6 postorder: " + postorderTraversal(root6)); // [4,5,2,6,7,3,1]
// 测试用例7:复杂树
int[] inorder7 = {1,2,3,4,5,6,7,8,9};
int[] postorder7 = {1,3,2,5,7,6,9,8,4};
TreeNode root7 = solution.buildTree(inorder7, postorder7);
System.out.println("Test 7 inorder: " + inorderTraversal(root7)); // [1,2,3,4,5,6,7,8,9]
System.out.println("Test 7 postorder: " + postorderTraversal(root7)); // [1,3,2,5,7,6,9,8,4]
}
}
关键点
-
构造顺序:
- 必须先构造右子树,再构造左子树
- 因为后序遍历从后往前时,右子树的根先出现
-
HashMap优化:
- 避免每次在中序数组中线性搜索根节点位置
- 将时间复杂度从O(n²)优化到O(n)
-
索引管理:
postIndex全局变量跟踪后序遍历位置- 中序遍历使用
起始和结束索引界定子树范围
-
边界条件:
- 空数组返回null
- 单节点树正确处理
- 递归终止条件:
inStart > inEnd
常见问题
-
为什么必须先构造右子树?
- 后序遍历顺序:[左子树序列, 右子树序列, 根]
- 从后往前遍历时,顺序变为:根, 右子树根, 左子树根
- 所以必须先处理右子树,才能正确获取左子树的根
-
如果没有HashMap优化会怎样?
- 每次都要在中序数组中线性搜索根节点位置
- 最坏情况下(链表状树)时间复杂度为O(n²)
-
能否从前序和中序构造?
- 可以!LeetCode 105题就是这个问题
- 区别在于:前序遍历从前往后,先构造左子树再构造右子树
-
为什么不能仅用后序遍历构造?
- 后序遍历无法确定左右子树的边界
- 需要
中序遍历来分割左右子树

463

被折叠的 条评论
为什么被折叠?



