leetcode106.从中序与后序遍历序列构造二叉树:索引定位与递归分治的完美配合

一、题目深度解析与核心挑战

在二叉树的重建问题中,"从中序与后序遍历序列构造二叉树"是一道经典的递归分治题目。题目要求我们根据一棵二叉树的中序遍历序列和后序遍历序列,重建出该二叉树的结构。这道题的核心难点在于如何利用两种遍历序列的特性,快速定位子树的根节点,并递归构建左右子树。

遍历序列特性回顾:

  • 中序遍历(Inorder):左-根-右,根节点将序列分为左右子树
  • 后序遍历(Postorder):左-右-根,最后一个元素是当前子树的根节点

示例输入输出:

输入:

复制代码
中序 inorder = [9,3,15,20,7]
后序 postorder = [9,15,7,20,3]

输出:

复制代码
    3
   / \
  9  20
    /  \
   15   7

重建的关键在于每次通过后序的最后一个元素确定根节点,再通过中序分割左右子树。

二、递归解法的核心实现与数据结构设计

完整递归代码实现

java 复制代码
/**
 * 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 {
    Map<Integer, Integer> map; // 存储中序值到索引的映射
    
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        map = new HashMap<>();
        for (int i = 0; i < inorder.length; i++) {
            map.put(inorder[i], i); // 预处理中序索引,O(n)时间
        }
        return findNode(inorder, 0, inorder.length, postorder, 0, postorder.length);
    }
    
    public TreeNode findNode(int[] inorder, int inBegin, int inEnd, 
                             int[] postorder, int postBegin, int postEnd) {
        if (inBegin >= inEnd || postBegin >= postEnd) {
            return null; // 子数组为空,返回null
        }
        // 后序最后一个元素是当前子树的根节点
        int rootVal = postorder[postEnd - 1]; 
        int rootIndex = map.get(rootVal); // 中序中根节点的索引
        
        TreeNode root = new TreeNode(rootVal); // 创建根节点
        
        // 计算左子树长度:中序中根节点左边的元素个数
        int lenLeft = rootIndex - inBegin; 
        
        // 递归构建左子树:中序[inBegin, rootIndex),后序[postBegin, postBegin+lenLeft)
        root.left = findNode(inorder, inBegin, rootIndex, 
                             postorder, postBegin, postBegin + lenLeft);
        
        // 递归构建右子树:中序[rootIndex+1, inEnd),后序[postBegin+lenLeft, postEnd-1)
        root.right = findNode(inorder, rootIndex + 1, inEnd, 
                              postorder, postBegin + lenLeft, postEnd - 1);
        
        return root;
    }
}

核心数据结构设计:

  1. HashMap映射表

    • 作用:快速查找中序遍历中值对应的索引(O(1)时间)
    • 预处理:遍历中序数组,将每个值与其索引存入map
    • 关键价值:避免每次查找根节点索引时遍历中序数组,将时间复杂度从O(n²)优化到O(n log n)
  2. 递归函数参数

    • inBegin/inEnd:中序数组当前处理的子数组范围(左闭右开)
    • postBegin/postEnd:后序数组当前处理的子数组范围(左闭右开)
    • 意义:通过索引范围划分当前子树的左右子树区域

三、核心问题解析:索引定位与递归分治过程

1. 根节点定位的核心逻辑

后序遍历的根节点特性
java 复制代码
int rootVal = postorder[postEnd - 1]; // 后序最后一个元素是根节点
int rootIndex = map.get(rootVal); // 中序中根节点的位置
  • 后序特性:后序遍历的最后一个元素必定是当前子树的根节点(左右子树遍历完才访问根)
  • 中序分割:根节点在中序中的位置将序列分为左子树(左边元素)和右子树(右边元素)
示例说明:
  • 后序数组[9,15,7,20,3]的最后一个元素是3,确定根节点为3
  • 中序数组[9,3,15,20,7]中3的索引是1,左边是左子树[9],右边是右子树[15,20,7]

2. 左右子树的索引划分

左子树范围确定
java 复制代码
int lenLeft = rootIndex - inBegin; // 左子树元素个数
// 后序左子树范围:postBegin 到 postBegin + lenLeft
root.left = findNode(inorder, inBegin, rootIndex, postorder, postBegin, postBegin + lenLeft);
  • 中序左子树 :从inBeginrootIndex(左闭右开,不包含根节点)
  • 后序左子树 :后序中左子树的元素个数与中序左子树相同,起始索引为postBegin,结束索引为postBegin + lenLeft
右子树范围确定
java 复制代码
// 中序右子树:从rootIndex+1到inEnd
// 后序右子树:左子树之后到postEnd-1(因为postEnd-1是当前根节点,右子树不包含根)
root.right = findNode(inorder, rootIndex + 1, inEnd, postorder, postBegin + lenLeft, postEnd - 1);
  • 关键公式:后序中右子树的起始索引 = 左子树结束索引(postBegin + lenLeft)
  • 边界处理 :右子树的后序结束索引是postEnd - 1(根节点已被处理,不包含在右子树中)

3. 递归终止条件

java 复制代码
if (inBegin >= inEnd || postBegin >= postEnd) {
    return null;
}
  • 触发场景:当子数组长度为0(inBegin == inEnd或postBegin == postEnd)
  • 逻辑意义:表示当前子树不存在,返回null作为叶子节点的子节点

四、递归分治流程模拟:以示例输入为例

示例输入:

  • 中序:[9,3,15,20,7](索引0-4)
  • 后序:[9,15,7,20,3](索引0-4)

详细递归过程:

  1. 第一次调用(构建整棵树)

    • inBegin=0, inEnd=5;postBegin=0, postEnd=5
    • 根节点:postorder[4]=3,中序索引1
    • 左子树长度:1-0=1(元素9)
    • 右子树长度:5-1-1=3(元素15,20,7)
  2. 构建左子树

    • 中序范围[0,1],后序范围[0,1]
    • 根节点:postorder[0]=9,中序索引0
    • 左右子树长度均为0,递归终止,左子树为叶子节点9
  3. 构建右子树

    • 中序范围[2,5](元素15,20,7),后序范围[1,4](元素15,7,20)
    • 根节点:postorder[3]=20,中序索引3
    • 左子树长度:3-2=1(元素15),右子树长度:5-3-1=1(元素7)
  4. 右子树的左子树(15)

    • 中序范围[2,3],后序范围[1,2]
    • 根节点:postorder[1]=15,中序索引2,左右子树为空,构建叶子节点15
  5. 右子树的右子树(7)

    • 中序范围[4,5],后序范围[2,4]
    • 根节点:postorder[3]=7(注意:后序范围[2,4)是索引2和3,值为7和20?这里需要修正,实际后序右子树范围应为[1+1=2,4],即postorder[2]=7,postorder[3]=20?原示例后序应为[9,15,7,20,3],右子树后序范围是postBegin+lenLeft=0+1=1到postEnd-1=4-1=3,即postorder[1...3]=[15,7,20],根节点是20(postorder[3]),中序索引3,左边是15(索引2),右边是7(索引4)。所以右子树的右子树后序范围是postBegin+lenLeft=1+1=2到postEnd-1=3,即postorder[2...3]=[7,20],根节点是20?这里可能之前的模拟有误,正确流程应严格按照代码逻辑,后序右子树的结束索引是postEnd-1,即当前子树的根节点位置前一位。

最终构建的树结构:

复制代码
    3
   / \
  9  20
    /  \
   15   7

五、算法复杂度分析

1. 时间复杂度

  • O(n):每个节点仅被创建一次,HashMap预处理O(n),每次递归分割子数组O(1)
  • 分治策略下,每个层级的总操作数为O(n),总共有O(log n)层(平衡树),最坏O(n)层(链表树),总体仍为O(n)

2. 空间复杂度

  • O(n):HashMap存储n个元素,递归栈深度O(n)(最坏情况树退化为链表)

3. 核心优化点

  • HashMap索引预处理:将中序索引查找从O(n)优化到O(1),避免双重循环
  • 分治策略:通过索引范围划分,每次递归将问题规模减半,符合分治思想

六、核心技术点总结:索引定位的三大关键步骤

1. 根节点的唯一性定位

  • 后序特性:最后一个元素是根节点,确保每次递归有且仅有一个根节点
  • 中序分割:根节点在中序中的位置将序列分为左右子树,保证子问题独立性

2. 子树范围的数学推导

  • 左子树长度rootIndex - inBegin(中序左边元素个数)
  • 后序左子树范围:起始索引与中序相同,长度相同
  • 后序右子树范围:起始索引=左子树结束索引,结束索引=父后序结束索引-1

3. 递归终止的边界处理

  • 空数组判断:当子数组长度为0时,返回null,作为递归终止条件
  • 正确性保证:确保每个子树的左右边界正确,避免越界访问

七、常见误区与边界情况处理

1. 空树处理

  • 输入为空数组时,直接返回null,代码中inBegin >= inEnd自动处理

2. 单节点树

  • 中序和后序均只有一个元素,直接创建节点,递归终止条件正确处理

3. 完全左/右子树

  • 例如后序[1,2,3],中序[3,2,1],递归时正确划分左子树为空,右子树逐步构建

八、总结:递归分治在树重建中的设计哲学

本算法通过"后序定根-中序分治-递归构建"的三步策略,完美解决了从中序与后序序列重建二叉树的问题。其核心设计哲学包括:

  1. 特性利用

    • 后序遍历的根节点特性(最后一个元素)
    • 中序遍历的左右子树划分特性
  2. 索引魔法

    • 通过HashMap实现中序值到索引的快速查找
    • 利用索引数学关系推导左右子树范围,避免数据拷贝
  3. 递归分治

    • 将原问题分解为左右子树的重建子问题
    • 通过索引范围传递,实现O(n)时间复杂度

这种解法不仅高效,而且逻辑清晰,充分体现了递归分治在树结构问题中的优势。理解索引定位的数学推导和递归边界的处理,是掌握此类问题的关键。在实际应用中,这种分治思想还可迁移到前序与中序重建、不同遍历序列的树重建等问题中,具有很强的通用性。

相关推荐
louisgeek1 小时前
Java 插入排序之希尔排序
java
小兵张健1 小时前
用户、资金库表和架构设计
java·后端·架构
洛小豆1 小时前
ConcurrentHashMap.size() 为什么“不靠谱”?答案比你想的复杂
java·后端·面试
asom221 小时前
LeetCode Hot100(矩阵)
算法·leetcode·矩阵
蒟蒻小袁1 小时前
力扣面试150题--二叉树的右视图
算法·leetcode·面试
一块plus1 小时前
当 Bifrost 与 Hydration 携手:Gigadot 能为 Polkadot DeFi 带来哪些新可能?
算法·架构
琢磨先生David1 小时前
Java 访问者模式深度重构:从静态类型到动态行为的响应式设计实践
java·设计模式·访问者模式
进击的小白菜2 小时前
LeetCode 215:数组中的第K个最大元素 - 两种高效解法详解
java·算法·leetcode
云道轩2 小时前
重新测试deepseek Jakarta EE 10编程能力
java·deepseek
6269602 小时前
【报错】Error attempting to get column ‘created_time‘ from result set.解决方法
java·jdk