LeetCode 105|从前序与中序遍历序列构造二叉树,3 种 JS 解法详解

1 阅读4分钟

题目:105. 从前序与中序遍历序列构造二叉树 | 难度:中等

一、题目描述

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

示例 1

plaintext

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

示例 2

plaintext

输入:preorder = [-1], inorder = [-1]
输出:[-1]

核心遍历规律(解题关键)

  1. 前序遍历(根 → 左 → 右) :数组第一个元素永远是当前子树的根节点
  2. 中序遍历(左 → 根 → 右) :根节点左侧是左子树全部节点,右侧是右子树全部节点

利用以上特性,我们就可以不断划分左右子树,递归构建二叉树。

二、解法一:基础递归(数组切片分割,新手友好)

思路

  1. 前序数组首元素作为根节点;
  2. 在中序数组找到根节点位置,切分出左、右子树的中序数组;
  3. 根据左子树节点数量,切分出左、右子树的前序数组;
  4. 递归构建左子树、右子树。

JavaScript 代码

javascript

运行

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {number[]} preorder
 * @param {number[]} inorder
 * @return {TreeNode}
 */
var buildTree = function(preorder, inorder) {
    // 递归终止条件:数组为空,代表空子树
    if (!preorder.length) return null;

    // 前序第一个元素 = 当前根节点
    const rootVal = preorder[0];
    const root = new TreeNode(rootVal);
    // 找到根节点在中序遍历中的下标
    const midIndex = inorder.indexOf(rootVal);

    // 递归构建左子树
    root.left = buildTree(
        preorder.slice(1, midIndex + 1),
        inorder.slice(0, midIndex)
    );
    // 递归构建右子树
    root.right = buildTree(
        preorder.slice(midIndex + 1),
        inorder.slice(midIndex + 1)
    );
    return root;
};

优缺点

✅ 优点:逻辑直观,代码简短,非常适合初学者理解思路❌ 缺点:

  1. slice 不断创建新数组,内存开销大;
  2. indexOf 线性查找,每次查找根节点需要 O (n);大数据量下效率较差。

三、解法二:哈希表优化递归(面试推荐最优解)

思路优化点

  1. 不切割数组:改用下标区间 [left, right] 划定子树范围,省去数组拷贝;
  2. 哈希 Map 预存映射:提前记录「节点值 → 中序数组下标」,查找根节点下标 O (1);
  3. 使用指针顺序遍历前序数组,严格遵循「根→左→右」构建顺序。

JavaScript 代码

javascript

运行

var buildTree = function(preorder, inorder) {
    // 建立值 -> 中序索引的哈希映射,快速查找
    const valToIndex = new Map();
    inorder.forEach((val, idx) => {
        valToIndex.set(val, idx);
    });
    // 前序遍历指针,全局共享,依次取根节点
    let preIndex = 0;

    /**
     * @param {number} left 当前子树在中序数组左边界
     * @param {number} right 当前子树在中序数组右边界
     */
    const dfs = (left, right) => {
        if (left > right) return null;
        // 取出当前根节点
        const rootVal = preorder[preIndex++];
        const root = new TreeNode(rootVal);
        // 获取根在中序的位置
        const mid = valToIndex.get(rootVal);
        // 先构建左子树,再构建右子树(契合前序遍历顺序)
        root.left = dfs(left, mid - 1);
        root.right = dfs(mid + 1, right);
        return root;
    };

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

复杂度分析

  • 时间复杂度:O(n),每个节点只遍历一次,哈希查找 O (1)
  • 空间复杂度:O(n),哈希表存储所有节点 + 递归调用栈

⭐ 面试首选写法,兼顾可读性与性能

四、解法三:迭代栈实现(非递归,进阶思路)

递归本质是程序调用栈,我们可以手动维护栈,不用递归函数实现构建。

思路

  1. 前序第一个元素作为根节点,入栈;设置中序遍历指针;

  2. 依次遍历后续前序节点:

    • 如果栈顶节点值!= 当前中序指针的值:新节点作为栈顶左孩子;
    • 如果相等:持续弹出栈,找到最后一个匹配中序值的节点,新节点作为它的右孩子;
  3. 新节点持续入栈,循环完成整棵树构建。

JavaScript 代码

javascript

运行

var buildTree = function(preorder, inorder) {
    if (!preorder.length) return null;
    const root = new TreeNode(preorder[0]);
    const stack = [root];
    let inIndex = 0;

    for (let i = 1; i < preorder.length; i++) {
        const currVal = preorder[i];
        const currNode = new TreeNode(currVal);
        let topNode = stack[stack.length - 1];

        if (topNode.val !== inorder[inIndex]) {
            // 还没遇到中序对应节点,挂左子树
            topNode.left = currNode;
        } else {
            // 不断出栈,寻找可以挂载右孩子的父节点
            while (stack.length && stack[stack.length - 1].val === inorder[inIndex]) {
                topNode = stack.pop();
                inIndex++;
            }
            topNode.right = currNode;
        }
        stack.push(currNode);
    }
    return root;
};

优缺点

✅ 优点:消除递归调用栈溢出风险;考察对二叉树遍历深层次理解❌ 缺点:逻辑绕,代码不易理解,日常业务、面试不做首选

五、三种方案横向对比

表格

解法实现方式难度适用场景
基础递归切片递归 + slice 分割数组初学理解思路,练习算法逻辑
哈希优化递归递归 + Map + 下标边界⭐⭐面试答题,最优综合方案
栈迭代循环 + 手动栈⭐⭐⭐进阶拓展,深挖遍历本质