【力扣100题】37.从前序与中序遍历序列构造二叉树

从前序与中序遍历序列构造二叉树

题目描述

给定两个整数数组 preorderinorder,其中 preorder 是二叉树的先序遍历,inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

示例 1:

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

树结构:
      3
     / \
    9   20
       / \
      15  7

示例 2:

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

提示:

  • 1 <= preorder.length <= 3000
  • inorder.length == preorder.length
  • -3000 <= preorder[i], inorder[i] <= 3000
  • preorder 和 inorder 均无重复元素
  • inorder 均出现在 preorder

解题思路总览

方法 思路 时间复杂度 空间复杂度 适用场景
方法一:递归 + HashMap 前序找根,中序分左右,递归构建 O(n) O(n) 面试首选
方法二:递归 + 线性查找 同上,但中序查找用线性扫描 O(n^2) O(h) n 较小时使用

核心原理: 前序遍历的第一个元素是根节点,在中序遍历中找到根节点后,左边是左子树,右边是右子树


方法一:递归 + HashMap(推荐)

思路

利用前序遍历和中序遍历的性质:

  1. 前序遍历顺序:根 - 左子树 - 右子树
  2. 中序遍历顺序:左子树 - 根 - 右子树

所以:

  • 前序遍历的第一个元素就是根节点
  • 在中序遍历中找到根节点的位置,左边的元素构成左子树,右边的元素构成右子树
  • 递归处理左右子树

为了快速找到中序遍历中根节点的位置,用 HashMap 存储中序遍历中每个值对应的索引。

完整代码

cpp 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    unordered_map<int, int> index;  // 存储中序遍历中每个值的位置

    TreeNode* build(vector<int>& preorder, vector<int>& inorder,
                    int preLeft, int preRight,
                    int inLeft, int inRight) {
        if (preLeft > preRight) return NULL;  // 递归终止条件

        // 1. 前序遍历的第一个元素是根节点
        int preorderRoot = preLeft;
        TreeNode* root = new TreeNode(preorder[preorderRoot]);

        // 2. 在中序遍历中找到根节点的位置
        int inorderRoot = index[preorder[preorderRoot]];

        // 3. 计算左子树的节点数量
        int len = inorderRoot - inLeft;

        // 4. 递归构建左子树和右子树
        root->left = build(preorder, inorder,
                          preorderRoot + 1,                    // 前序:左子树的起点
                          preorderRoot + len,                  // 前序:左子树的终点
                          inLeft, inorderRoot - 1);            // 中序:左子树范围

        root->right = build(preorder, inorder,
                           preorderRoot + len + 1, preRight,   // 前序:右子树起点
                           inorderRoot + 1, inRight);          // 中序:右子树范围

        return root;
    }

    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int n = preorder.size();

        // 构建中序遍历的值到索引的映射
        for (int i = 0; i < n; i++) {
            index[inorder[i]] = i;
        }

        return build(preorder, inorder, 0, n - 1, 0, n - 1);
    }
};

算法流程图

preorder = [3,9,20,15,7], inorder = [9,3,15,20,7] 为例:

复制代码
preorder = [3, 9, 20, 15, 7]  →  根 左子树 右子树
inorder  = [9, 3, 15, 20, 7]  →  左子树 根 右子树

构建过程:

Step 1: 处理根节点
  preorder[0] = 3 是根节点
  在 inorder 中找到 3,位置是 index=1

Step 2: 分裂左右子树
  左子树节点数 = inorderRoot - inLeft = 1 - 0 = 1
  即 inorder[0] = 9 是左子树的节点

  左子树:
    preorder: [9]      (preorder[1] 到 preorder[1])
    inorder:  [9]      (inorder[0] 到 inorder[0])

  右子树:
    preorder: [20,15,7]  (preorder[2] 到 preorder[4])
    inorder:  [15,20,7]  (inorder[2] 到 inorder[4])

Step 3: 递归构建左子树(节点9)
  preorder[1] = 9 是根节点
  在 inorder 中找到 9,位置是 index=0
  左子树节点数 = 0 - 0 = 0,左子树为空
  右子树节点数 = 0 - 0 = 0,右子树为空
  返回叶子节点 9

Step 4: 递归构建右子树(节点20)
  preorder[2] = 20 是根节点
  在 inorder 中找到 20,位置是 index=3

  左子树节点数 = 3 - 2 = 1
  即 inorder[2] = 15 是左子树的节点

  左子树:
    preorder: [15]     (preorder[3] 到 preorder[3])
    inorder:  [15]     (inorder[2] 到 inorder[2])

  右子树:
    preorder: [7]      (preorder[4] 到 preorder[4])
    inorder:  [7]      (inorder[4] 到 inorder[4])

Step 5: 递归构建节点15(左子树的叶子)
  左右子树为空,返回叶子节点 15

Step 6: 递归构建节点7(右子树的叶子)
  左右子树为空,返回叶子节点 7

最终树结构:
      3
     / \
    9   20
       / \
      15  7

逐行解析

cpp 复制代码
unordered_map<int, int> index;  // 存储中序遍历中每个值的位置
  • 用 HashMap 存储中序遍历中每个值对应的索引。
  • 作用:在 O(1) 时间内找到根节点在中序遍历中的位置,而不需要每次线性搜索。
cpp 复制代码
TreeNode* build(vector<int>& preorder, vector<int>& inorder,
                int preLeft, int preRight,
                int inLeft, int inRight) {
  • 递归函数,参数为前序和中序遍历数组,以及当前处理范围的左右边界索引。
  • [preLeft, preRight] 表示当前处理的前序遍历范围
  • [inLeft, inRight] 表示当前处理的中序遍历范围
cpp 复制代码
    if (preLeft > preRight) return NULL;  // 递归终止条件
  • 当前序遍历的左边界大于右边界时,说明当前范围为空,返回 NULL。
cpp 复制代码
    int preorderRoot = preLeft;
    TreeNode* root = new TreeNode(preorder[preorderRoot]);
  • 前序遍历的第一个元素(preorder[preLeft])就是当前子树的根节点。
cpp 复制代码
    int inorderRoot = index[preorder[preorderRoot]];
  • 用 HashMap 在 O(1) 时间内找到根节点在中序遍历中的位置。
cpp 复制代码
    int len = inorderRoot - inLeft;
  • 计算左子树的节点数量。
  • inorderRoot 是根在中序中的位置,inLeft 是当前中序范围的左边界。
  • 两者差值就是左子树的节点数。
cpp 复制代码
    root->left = build(preorder, inorder,
                      preorderRoot + 1,                    // 前序:左子树起点
                      preorderRoot + len,                 // 前序:左子树终点
                      inLeft, inorderRoot - 1);            // 中序:左子树范围
  • 递归构建左子树:
    • 前序范围:[preorderRoot + 1, preorderRoot + len](根之后 len 个元素)
    • 中序范围:[inLeft, inorderRoot - 1](根位置之前的所有元素)
cpp 复制代码
    root->right = build(preorder, inorder,
                       preorderRoot + len + 1, preRight,   // 前序:右子树起点
                       inorderRoot + 1, inRight);          // 中序:右子树范围
  • 递归构建右子树:
    • 前序范围:[preorderRoot + len + 1, preRight](左子树之后的所有元素)
    • 中序范围:[inorderRoot + 1, inRight](根位置之后的所有元素)

复杂度分析

复杂度 说明
时间 O(n) 每个节点访问一次
空间 O(n) HashMap 存储 n 个节点的索引 + 递归栈深度 O(h)

优点: 利用 HashMap 将中序查找从 O(n) 优化到 O(1),整体时间复杂度 O(n)
缺点: 需要额外的 O(n) 空间存储 HashMap


方法二:递归 + 线性查找

思路

与方法一相同,但不使用 HashMap,每次递归时线性查找根节点在中序遍历中的位置。

完整代码

cpp 复制代码
class Solution {
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        if (preorder.empty()) return NULL;

        int rootVal = preorder[0];
        TreeNode* root = new TreeNode(rootVal);

        // 在中序遍历中找到根节点的位置
        int inorderRoot = 0;
        while (inorder[inorderRoot] != rootVal) {
            inorderRoot++;
        }

        // 构建左子树
        vector<int> preLeft(preorder.begin() + 1,
                           preorder.begin() + 1 + inorderRoot);
        vector<int> inLeft(inorder.begin(),
                          inorder.begin() + inorderRoot);
        root->left = buildTree(preLeft, inLeft);

        // 构建右子树
        vector<int> preRight(preorder.begin() + 1 + inorderRoot,
                            preorder.end());
        vector<int> inRight(inorder.begin() + inorderRoot + 1,
                          inorder.end());
        root->right = buildTree(preRight, inRight);

        return root;
    }
};

复杂度分析

复杂度 说明
时间 O(n^2) 每次递归线性查找 O(n),共 O(n) 次递归
空间 O(h) 递归栈深度,但创建了 O(n) 的额外向量空间

优点: 代码更直观,容易理解
缺点: 时间复杂度较高,不适合大规模数据


两种方法对比

维度 方法一 HashMap 方法二 线性查找
代码复杂度 中等 简单
时间复杂度 O(n) O(n^2)
空间复杂度 O(n) O(h) + O(n) 向量
面试推荐度 首选 简化版/教学用
n 较大时 适用 不适用

面试追问 FAQ

问题 解答
Q1:为什么前序遍历的第一个元素就是根节点? 前序遍历的顺序是"根-左-右",所以第一个元素一定是树的根节点。
Q2:如何确定左子树和右子树的范围? 在中序遍历中找到根节点的位置,根左边的元素构成左子树,根右边的元素构成右子树。根节点在中序中的索引 - 中序左边界 = 左子树的节点数。
Q3:为什么需要 HashMap? 避免每次递归时都线性搜索中序数组来找根节点位置。HashMap 将查找从 O(n) 优化到 O(1),整体时间复杂度从 O(n^2) 优化到 O(n)。
Q4:如何理解递归参数中的索引范围? [preLeft, preRight][inLeft, inRight] 定义了当前处理的子树在前序和中序遍历中对应的区间。每次递归时根据已知的根节点位置,划分出左右子树的区间传递给下一层递归。
Q5:如果不用递归,能否用迭代实现? 可以使用栈模拟递归,但代码复杂度较高。递归的思路最清晰,面试中优先写出递归解法。
Q6:如何从后序和中序遍历序列构造二叉树? 思路相同。后序遍历的最后一个元素是根节点,在中序中分左右子树后,递归处理。

相关题目

题目 难度 关键点
105. 从前序与中序遍历序列构造二叉树 中等 前序+中序构造树
106. 从中序与后序遍历序列构造二叉树 中等 后序+中序构造树
889. 根据前序和后序遍历构造二叉树 中等 前序+后序构造树(不唯一)
654. 最大二叉树 中等 前序遍历构造树
剑指 Offer 07. 重建二叉树 中等 前序+中序构造树

总结

要点 说明
核心原理 前序找根,中序分左右,递归构建
关键技巧 用 HashMap 将中序查找从 O(n) 优化到 O(1)
时间复杂度 O(n)
空间复杂度 O(n) HashMap + O(h) 递归栈
递归终止条件 preLeft > preRight 时返回 NULL

相关推荐
zyq99101_11 小时前
递归与动态规划实战代码解析
python·算法·蓝桥杯
蜡笔小马2 小时前
08.C++设计模式-享元模式
c++·设计模式·享元模式
橘白3162 小时前
rl笔记(一):策略梯度更新算法推导
人工智能·算法·机器人·强化学习
hhhhhaaa2 小时前
多节点矩阵式任务系统:统一配置中心与动态规则引擎架构设计
后端·算法·架构
吃着火锅x唱着歌2 小时前
LeetCode 739.每日温度
算法·leetcode·职场和发展
如竟没有火炬2 小时前
去除重复字母——贪心+单调栈
开发语言·数据结构·python·算法·leetcode·深度优先
小侯不躺平.2 小时前
C++ Boost库【4】 --分词器的使用
c++·windows·microsoft
薛定e的猫咪2 小时前
【ICML 2025】MODULI:基于扩散模型解锁离线多目标强化学习的偏好泛化
人工智能·学习·算法·机器学习
码农-阿杰2 小时前
Java 线程中断机制深度解析:从 API 到底层 C++ 实现
java·开发语言·c++