【LeetCode Hot100 刷题日记 (47/100)】105. 从前序与中序遍历序列构造二叉树 —— 分治 + 哈希表优化递归🌳

7 阅读5分钟

📌 题目链接:105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)

🔍 难度:中等 | 🏷️ 标签:树、深度优先搜索(DFS)、分治、哈希表

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(n)


🧠 题目分析

给定一棵二叉树的 前序遍历中序遍历 序列,要求我们 还原出原始的二叉树结构 并返回其根节点。

这是一道经典的 树重建问题。关键在于理解前序和中序遍历的结构性质:

  • 前序遍历(Preorder)[根, 左子树, 右子树]
  • 中序遍历(Inorder)[左子树, 根, 右子树]

利用这两个性质,我们可以:

  1. 从前序中拿到当前子树的 根节点
  2. 在中序中定位该根节点,从而 划分左右子树的范围
  3. 递归地对左右子树重复上述过程。

⚠️ 注意题目保证:数组中无重复元素,因此每个值唯一对应一个节点,便于使用哈希表快速定位。


⚙️ 核心算法及代码讲解

核心思想分治 + 递归 + 哈希表优化

我们采用 递归分治 的策略,每次从 preorder 中取出当前子树的根,再通过 inorder 划分左右子树区间。

为了 避免每次线性扫描中序数组找根的位置(会导致 O(n²) 时间),我们提前用 哈希表 存储 inorder 中每个值对应的索引,实现 O(1) 定位

C++ 核心函数详解(带行注释)

TreeNode* myBuildTree(
    const vector<int>& preorder,
    const vector<int>& inorder,
    int preorder_left,   // 当前子树在 preorder 中的左边界(包含)
    int preorder_right,  // 当前子树在 preorder 中的右边界(包含)
    int inorder_left,    // 当前子树在 inorder 中的左边界(包含)
    int inorder_right    // 当前子树在 inorder 中的右边界(包含)
) {
    // 递归终止条件:区间无效
    if (preorder_left > preorder_right) {
        return nullptr;
    }

    // 前序遍历的第一个元素即为当前子树的根节点
    int preorder_root = preorder_left;
    // 通过哈希表 O(1) 找到根在中序中的位置
    int inorder_root = index[preorder[preorder_root]];

    // 创建根节点
    TreeNode* root = new TreeNode(preorder[preorder_root]);

    // 计算左子树的节点数量(关键!用于划分前序区间)
    int size_left_subtree = inorder_root - inorder_left;

    // 递归构建左子树:
    // - 前序区间:[preorder_left + 1, preorder_left + size_left_subtree]
    // - 中序区间:[inorder_left, inorder_root - 1]
    root->left = myBuildTree(
        preorder, inorder,
        preorder_left + 1,
        preorder_left + size_left_subtree,
        inorder_left,
        inorder_root - 1
    );

    // 递归构建右子树:
    // - 前序区间:[preorder_left + size_left_subtree + 1, preorder_right]
    // - 中序区间:[inorder_root + 1, inorder_right]
    root->right = myBuildTree(
        preorder, inorder,
        preorder_left + size_left_subtree + 1,
        preorder_right,
        inorder_root + 1,
        inorder_right
    );

    return root;
}

💡 关键点size_left_subtree 是连接前序与中序区间的桥梁!
因为左右子树的节点数在两种遍历中是相同的,所以可以用中序中左子树长度来切分前序。


🧩 解题思路(分步骤)

  1. 预处理:遍历 inorder,建立 value → index 的哈希映射 index

  2. 递归入口:调用 myBuildTree(preorder, inorder, 0, n-1, 0, n-1)

  3. 递归逻辑

    • 若当前区间无效(左 > 右),返回 nullptr
    • preorder[left] 作为根。
    • 用哈希表查根在 inorder 中的位置 inorder_root
    • 计算左子树节点数:size = inorder_root - inorder_left
    • 递归构建左子树(前序从 left+1 开始,共 size 个)。
    • 递归构建右子树(前序从 left+1+size 开始到右边界)。
  4. 返回根节点,完成当前子树构建。


📊 算法分析

项目分析
时间复杂度O(n):每个节点被访问一次,哈希查找 O(1)
空间复杂度O(n):哈希表 O(n) + 递归栈 O(h),最坏 h = n(退化为链表)→ 总 O(n)
面试考点树的遍历性质、分治思想、递归设计、哈希优化、边界处理
易错点区间开闭搞错、左右子树长度计算错误、未处理空树

🎯 高频面试变形

  • 中序 + 后序 构造二叉树(LeetCode 106)
  • 判断两序列是否能构成同一棵树
  • 序列含重复值时如何处理?(需额外信息,如节点指针或唯一ID)

💻 代码

✅C++

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// 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 {
private:
    unordered_map<int, int> index;

public:
    TreeNode* myBuildTree(const vector<int>& preorder, const vector<int>& inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
        if (preorder_left > preorder_right) {
            return nullptr;
        }
        
        // 前序遍历中的第一个节点就是根节点
        int preorder_root = preorder_left;
        // 在中序遍历中定位根节点
        int inorder_root = index[preorder[preorder_root]];
        
        // 先把根节点建立出来
        TreeNode* root = new TreeNode(preorder[preorder_root]);
        // 得到左子树中的节点数目
        int size_left_subtree = inorder_root - inorder_left;
        // 递归地构造左子树,并连接到根节点
        root->left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
        // 递归地构造右子树,并连接到根节点
        root->right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
        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 myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    Solution sol;
    
    // 测试用例 1
    vector<int> preorder1 = {3,9,20,15,7};
    vector<int> inorder1 = {9,3,15,20,7};
    TreeNode* root1 = sol.buildTree(preorder1, inorder1);
    // 验证:应输出 [3,9,20,null,null,15,7]

    // 测试用例 2
    vector<int> preorder2 = {-1};
    vector<int> inorder2 = {-1};
    TreeNode* root2 = sol.buildTree(preorder2, inorder2);
    // 验证:应输出 [-1]

    // 此处省略树的打印函数(LeetCode 自动验证)
    // 实际可配合层序遍历打印验证结构

    return 0;
}

✅JavaScript

// JavaScript 版本(递归 + 哈希表)
var buildTree = function(preorder, inorder) {
    if (preorder.length === 0) return null;

    // 构建哈希表:值 -> 中序索引
    const indexMap = new Map();
    for (let i = 0; i < inorder.length; i++) {
        indexMap.set(inorder[i], i);
    }

    function build(preLeft, preRight, inLeft, inRight) {
        if (preLeft > preRight) return null;

        const rootVal = preorder[preLeft];
        const rootNode = new TreeNode(rootVal);

        const inRoot = indexMap.get(rootVal);
        const leftSize = inRoot - inLeft;

        rootNode.left = build(preLeft + 1, preLeft + leftSize, inLeft, inRoot - 1);
        rootNode.right = build(preLeft + leftSize + 1, preRight, inRoot + 1, inRight);

        return rootNode;
    }

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

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!