当前位置: 首页 > news >正文

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

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

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

遍历序列特性回顾:

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

示例输入输出:

输入:

中序 inorder = [9,3,15,20,7]
后序 postorder = [9,15,7,20,3]

输出:

    3/ \9  20/  \15   7

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

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

完整递归代码实现

/*** 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. 根节点定位的核心逻辑

后序遍历的根节点特性
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. 左右子树的索引划分

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

3. 递归终止条件

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)时间复杂度

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

相关文章:

  • 网络 :序列和反序列化
  • 使用Docker Compose部署Dify
  • Linux `|` 管道操作符深度解析与高阶应用指南
  • SOC-ESP32S3部分:12-2、编码器驱动
  • ae卡通打架烟雾特效
  • 梯度下降 损失景观 视频截图
  • 第十八章:数据治理之数据质量:“数据质量”不仅仅和“数据质量”有关
  • 在train和eval模式下性能差距的问题(本文聚焦于BatchNorm2d)
  • 指针数组和数组指针的区别
  • ssm-ham项目1
  • 人工智能赋能教育:重塑学习生态,开启智慧未来
  • 小白的进阶之路系列之四----人工智能从初步到精通pytorch自定义数据集上
  • day36 python神经网络训练
  • 【LLM】LLM源码阅读与分析工具DeepWiki项目
  • Qt环境的搭建
  • NextJS 项目,编译成功,但是启动失败的解决方案
  • Docker镜像存储路径迁移指南(解决磁盘空间不足问题)
  • 嵌入式学习笔记——day27
  • 22 程序控制语句详解:跳转控制(break、continue、goto)、死循环应用、程序控制编程实战
  • 支持单双及四像素模式的testpattern仿真
  • 网站建站 优化推广/凡科建站登录
  • 政府机关网站建设方案/百度关键词搜索热度查询
  • 提供微网站制作电话/百度上怎么发布作品
  • 北京星光灿烂影视有限公司/关键词优化平台有哪些
  • 专门做店铺转让的网站/超级seo工具
  • 网站制作需求分析/推广公司