【LeetCode刷题日记】106.从遍历序列重建二叉树:手撕递归边界,彻底搞懂左闭右闭 vs 左闭右开

🔥个人主页:北极的代码(欢迎来访)

🎬作者简介:java后端学习者

❄️个人专栏:苍穹外卖日记SSM框架深入JavaWeb

命运的结局尽可永在,不屈的挑战却不可须臾或缺!

前言:

大家好,我是代码不加冰,今天是我们的每日刷题时间,前几周我们一直在学习二叉树相关的算法题,感觉有点枯燥,想学一点新的知识,总感觉长时间写一个种类的算法题很无聊,所以我想加快下速度,或者同时进行后面的章节,回溯算法,或者贪心算法。

摘要:

本文介绍了如何根据二叉树的中序和后序遍历序列重构二叉树。通过分析示例,文章指出仅靠后序遍历无法确定节点间的父子关系,必须结合中序遍历来精确定位。核心解法是:1)取后序最后一个元素作为根节点;2)在中序中找到根节点位置并分割左右子树;3)根据中序分割结果对应分割后序数组;4)递归处理左右子树。文章详细讲解了边界切割方法(推荐左闭右开),并提供了Java实现代码,强调要注意递归终止条件和边界值的正确处理。该方法利用哈希表优化查找效率,时间复杂度为O(n)。

题目背景:

给定两个整数数组 inorderpostorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树

示例 1:

复制代码
输入:inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]
输出:[3,9,20,null,null,15,7]

示例 2:

复制代码
输入:inorder = [-1], postorder = [-1]
输出:[-1]

提示:

  • 1 <= inorder.length <= 3000
  • postorder.length == inorder.length
  • -3000 <= inorder[i], postorder[i] <= 3000
  • inorderpostorder 都由 不同 的值组成
  • postorder 中每一个值都在 inorder
  • inorder 保证是树的中序遍历
  • postorder 保证是树的后序遍历

题目解析:

拿到这个题目,我们要先搞清楚题目的意思,这个题目说是从中序与后序遍历序列构造二叉树,意思就是没给我们实际的二叉树,让我们自己跟据这个二叉树的中序遍历结果和后序遍历结果来反推出二叉树,那为什么要两种遍历方式呢:

两种不同的树,后序遍历结果却一样

情况一:

复制代码
    B
   / \
  A   C

后序遍历(左→右→根):A → C → B → [A, C, B]

情况二:

复制代码
    B
   /
  C
 /
A

这棵树只有左孩子(一路向左)。

后序遍历(左→右→根):

  1. 访问 A

  2. 访问 C

  3. 访问 B

    结果也是 [A, C, B]

发现

  • 情况一:[A, C, B] 中,A 是左子树(B的左孩子),C 是右子树(B的右孩子)

  • 情况二:[A, C, B] 中,A 是左子树的左子树,C 是 B 的左孩子

只给后序 [A, C, B],我们无法区分:

  • 到底 C 是 B 的左孩子还是右孩子?

  • 到底 A 和 C 是兄弟关系还是父子关系?

这时候必须用中序遍历来澄清:

  • 如果中序是 [A, B, C] → B 在中间,A 在左,C 在右 → 情况一(左右都有)

  • 如果中序是 [A, C, B] → B 在最后,A 和 C 都在 B 左边 → 情况二(一路向左)

光靠后序,只能知道"A 在整体的左边",但无法知道"A 是紧挨着根的左孩子,还是隔了好几层的左孙子"。中序遍历提供了"位置关系"的精确信息。


那么整体的题目要求我们就了解了,那具体该怎么实现呢

后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来再切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。

输入:

中序 [9, 3, 15, 20, 7]

后序 [9, 15, 7, 20, 3]

我们画个图走一遍:

  1. 看后序最后一个 :是 3。 → 根节点是 3

  2. 去中序找 3

    • 3 在中间。

    • 左边 [9] 是左子树。

    • 右边 [15, 20, 7] 是右子树。

  3. 处理左子树 (中序 [9],后序 [9]):

    • 只有一个 9。所以 3 的左孩子是 9。
  4. 处理右子树 (中序 [15, 20, 7],后序 [15, 7, 20]):

    • 看右子树的后序(15, 7, 20),最后一个 20 是右子树的根。

    • 去中序(15, 20, 7)找 20:

      • 左边 [15] 是左孩子。

      • 右边 [7] 是右孩子。

  5. 总结结构

    • 根:3

    • 3 的左:9

    • 3 的右:20

    • 20 的左:15

    • 20 的右:7

所以还原出来的树长这样:

复制代码
    3
   / \
  9  20
     / \
    15  7
  • 第一步:如果数组大小为零的话,说明是空节点了。

  • 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。

  • 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点

  • 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)

  • 第五步:切割后序数组,切成后序左数组和后序右数组

  • 第六步:递归处理左区间和右区间

难点大家应该发现了,就是如何切割,以及边界值找不好很容易乱套。

边界切割

此时应该注意确定切割的标准,是左闭右开,还有左开右闭,还是左闭右闭,这个就是不变量,要在递归中保持这个不变量。

在切割的过程中会产生四个区间,把握不好不变量的话,一会左闭右开,一会左闭右闭,必然乱套

在二分法中强调过循环不变量的重要性,在二分查找以及螺旋矩阵的求解中,坚持循环不变量非常重要,本题也是。

首先要切割中序数组,为什么先切割中序数组呢

切割点在后序数组的最后一个元素,就是用这个元素来切割中序数组的,所以必要先切割中序数组。

中序数组相对比较好切,找到切割点(后序数组的最后一个元素)在中序数组的位置,然后切割

接下来就要切割后序数组了

首先后序数组的最后一个元素指定不能要了,这是切割点 也是 当前二叉树中间节点的元素,已经用了。

后序数组的切割点怎么找

后序数组没有明确的切割元素来进行左右切割,不像中序数组有明确的切割点,切割点左右分开就可以了。

此时,中序数组切成了左中序数组和右中序数组,后序数组切割成左后序数组和右后序数组。

接下来可以递归了


图解切割(四个边界)

我们把要递归传给左右子树的范围画出来。

第一层:根节点 3

完整数组:

中序: [ (9) , 3 , (15,20,7) ]

后序: [ (9) , (15,7,20) , 3 ]

切割结果:

子树 中序范围 (inorder) 后序范围 (postorder)
左子树 [0 , 0] (只有9) [0 , 0] (只有9)
右子树 [2 , 4] (15,20,7) [1 , 3] (15,7,20)

右子树的内部(验证边界)

此时递归进入右子树,参数更新为:

  • inStart = 2, inEnd = 4

  • postStart = 1, postEnd = 3

右子树数组看起来像这样:

中序子数组: [15, 20, 7] (新索引 0~2,对应原索引 2~4)

后序子数组: [15, 7, 20] (新索引 0~2,对应原索引 1~3)

在这一层:

  • 根节点:后序最后一个 20 (原索引 3)

  • 在中序找 20:原索引是 3

  • 新左子树长度 = rootIndex - inStart = 3 - 2 = 1 (只有15)

4. 四个边界的计算公式(重点)

假设当前递归函数参数如下:

  • 中序:inStartinEnd

  • 后序:postStartpostEnd

  • 计算得:rootIndex (中序里根的位置),leftSize (左子树节点数)

此时有一个很重的点,就是中序数组大小一定是和后序数组的大小相同的(这是必然)。

中序数组我们都切成了左中序数组和右中序数组了,那么后序数组就可以按照左中序数组的大小来切割,切成左后序数组和右后序数组。

  1. 后序左右边界

    • 左子树[postStart , postStart + leftSize - 1]

    • 右子树[postStart + leftSize , postEnd - 1] (这里 -1 是因为要把当前的根 postEnd 去掉)

  2. 中序左右边界

    • 左子树[inStart , rootIndex - 1]

    • 右子树[rootIndex + 1 , inEnd]

6. 易错点提示

  • postEnd - 1 :千万不要忘了 -1,因为 postEnd 是当前树的根,传给子树必须去掉。

  • leftSize 用中序算 :千万不要试图用后序的指针减来减去算长度,必须用中序 (rootIndex - inStart)。

  • 空区间判断 :递归开头一定要判断 if (inStart > inEnd) return null;

总结一句:

先拿 rootIndexinStart 算出 左子树有几个节点 ,然后把 leftSize 加到 postStart 上,就能精准切出后序的左子树区间。这就是解决边界问题的唯一标准方法。


代码实现

这里用左闭右闭演示一下代码:
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 {
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        // 存储中序遍历中每个值的索引,方便快速查找根节点位置
        Map<Integer, Integer> indexMap = new HashMap<>();
        for (int i = 0; i < inorder.length; i++) {
            indexMap.put(inorder[i], i);
        }
        return build(inorder, 0, inorder.length - 1, 
                     postorder, 0, postorder.length - 1, 
                     indexMap);
    }
    
    private TreeNode build(int[] inorder, int inStart, int inEnd,
                          int[] postorder, int postStart, int postEnd,
                          Map<Integer, Integer> indexMap) {
        // 递归终止条件:区间为空
        if (inStart > inEnd || postStart > postEnd) {
            return null;
        }
        
        // 后序遍历的最后一个节点就是根节点
        int rootVal = postorder[postEnd];
        TreeNode root = new TreeNode(rootVal);
        
        // 在中序遍历中找到根节点的位置
        int rootIndex = indexMap.get(rootVal);
        
        // 左子树的节点个数
        int leftSize = rootIndex - inStart;
        
        // 递归构建左子树
        // 左子树的中序遍历区间:[inStart, rootIndex - 1]
        // 左子树的后序遍历区间:[postStart, postStart + leftSize - 1]
        root.left = build(inorder, inStart, rootIndex - 1,
                         postorder, postStart, postStart + leftSize - 1,
                         indexMap);
        
        // 递归构建右子树
        // 右子树的中序遍历区间:[rootIndex + 1, inEnd]
        // 右子树的后序遍历区间:[postStart + leftSize, postEnd - 1]
        root.right = build(inorder, rootIndex + 1, inEnd,
                          postorder, postStart + leftSize, postEnd - 1,
                          indexMap);
        
        return root;
    }
}
参数 含义
inorder 中序遍历数组 不变,全程使用
inStart 0 中序数组的起始索引(第一个元素)
inEnd inorder.length - 1 中序数组的结束索引(最后一个元素)
postorder 后序遍历数组 不变,全程使用
postStart 0 后序数组的起始索引(第一个元素)
postEnd postorder.length - 1 后序数组的结束索引(最后一个元素)
indexMap 值→索引的映射 方便快速查找根在中序的位置

为什么要传这些参数

因为递归函数需要知道当前处理的是哪一段区间

  • 第一次调用:处理整棵树 → 区间是 [0, 长度-1]

  • 递归左子树:处理左子树部分 → 区间会缩小,比如 [0, 2]

  • 递归右子树:处理右子树部分 → 区间会缩小,比如 [4, 6]

这四个索引(inStart, inEnd, postStart, postEnd)定义了当前递归层要处理的子树范围。

举例说明

用之前的例子:

  • inorder = [9, 3, 15, 20, 7],长度 = 5

  • postorder = [9, 15, 7, 20, 3],长度 = 5

第一次调用:

text

复制代码
inStart = 0
inEnd = 4   (因为 5-1=4)
postStart = 0
postEnd = 4 (因为 5-1=4)

这表示:整个中序数组的 [0, 4] 和整个后序数组的 [0, 4] 构成了当前要处理的树。

左闭右开版本的对比

如果你用左闭右开,第一次调用就是:

java

复制代码
return build(inorder, 0, inorder.length,      // 注意:是 length,不是 length-1
             postorder, 0, postorder.length, 
             indexMap);

区别:

  • 左闭右闭 :终点是 length - 1

  • 左闭右开 :终点是 length(不包含)

左闭右开的核心公式(简洁版)

区间类型 左子树后序 右子树后序
左闭右闭 [postStart, postStart + leftSize - 1] [postStart + leftSize, postEnd - 1]
左闭右开 [postStart, postStart + leftSize) [postStart + leftSize, postEnd - 1)

注意右子树的后序终点是 postEnd - 1 而不是 postEnd ,因为 postEnd - 1 是当前根节点,右子树不能包含它。

为什么左闭右开更好
  1. 长度直接算 :区间 [l, r) 的长度就是 r - l,不需要 +1

  2. 空区间判断简单l >= r 就是空,不用纠结 > 还是 >=

  3. leftSize 直接用postStart + leftSize 天然就是右子树起点,不用 +1

一个小坑提醒

左闭右开版本中,右子树的后序终点是 postEnd - 1 ,不是 postEnd

因为 postEnd - 1 才是当前树的根节点,传给右子树时必须排除它。这是左闭右开唯一需要小心的地方。

总结: 如果觉得边界总搞错,个人建议改成左闭右开,逻辑更顺,代码更短。

结语:如果对你有帮助,请**点赞,关注,收藏,**你的支持就是我最大的鼓励!

相关推荐
.魚肉2 小时前
Raft 共识算法 · 演示系统(多终端)
算法·go·raft·分布式系统
念恒123062 小时前
Python(while循环)
数据结构·python·算法
神奇小汤圆2 小时前
字节面试官:你知道Claude Code的多Agent实现机制吗?
算法
luck_bor2 小时前
Map&Stream流
java·开发语言
运筹vivo@2 小时前
LeetCode 2540. 最小公共值
算法·leetcode·职场和发展
小许同学记录成长2 小时前
轻量正射实现原理技术文档
算法·无人机
阿文的代码库2 小时前
如何在C++中使用标准库的智能指针
开发语言·c++·算法
用户298698530142 小时前
Java 统计 Word 文档中的单词数量
java·后端
城事漫游Molly2 小时前
方差分析(ANOVA)入门——比较三组或更多组均值的利器
大数据·算法·均值算法·论文笔记·科研统计