【每日算法】LeetCode 105. 从前序与中序遍历序列构造二叉树

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

对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎

1. 题目描述

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

假设树中没有重复的元素。

示例 1:

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

示例 2:

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

提示:

  • 1 <= preorder.length <= 3000
  • inorder.length == preorder.length
  • -3000 <= preorder[i], inorder[i] <= 3000
  • preorder 和 inorder 均保证为二叉树的有效遍历序列
  • preorder 和 inorder 中每个值都不同

2. 问题分析

2.1 二叉树遍历特性

  • 前序遍历(Preorder): 根节点 → 左子树 → 右子树
  • 中序遍历(Inorder): 左子树 → 根节点 → 右子树

2.2 核心规律

前序遍历序列的第一个元素一定是整棵树的根节点。在中序遍历序列中找到这个根节点,其左侧就是左子树的中序遍历结果,右侧就是右子树的中序遍历结果。知道了左右子树的节点数量后,就可以在前序遍历序列中划分出左右子树的前序遍历序列。

2.3 前端视角思考

在前端开发中,我们经常需要处理树形数据结构,例如:

  • DOM树的操作和渲染
  • React/Vue的虚拟DOM树
  • 组件树的状态管理
  • 文件目录树、菜单导航树等UI组件

理解如何构建和遍历树结构,对于开发复杂前端应用至关重要。

3. 解题思路

3.1 基本思路

  1. 确定根节点: preorder[0] 就是当前树的根节点
  2. 找到分割点: 在inorder中找到根节点的位置,将中序序列分为左子树和右子树
  3. 计算左右子树大小: 左子树节点数 = 根节点在中序序列中的索引 - 中序序列起始索引
  4. 递归构建 :
    • 左子树:preorder[1 : 左子树节点数+1], inorder[0 : 根节点索引]
    • 右子树:preorder[左子树节点数+1 : ], inorder[根节点索引+1 : ]

3.2 优化方向

  • 查找效率: 每次在中序数组中查找根节点位置如果是线性查找,时间复杂度为O(n),整体会达到O(n²)。使用哈希表存储中序数组的值到索引的映射,可以将查找时间降为O(1)。
  • 空间利用: 传递数组切片会创建新数组,增加空间开销。可以传递索引范围来避免数组复制。

3.3 复杂度分析

最优解: 递归+哈希表+索引传递法

  • 时间复杂度: O(n) - 每个节点只访问一次
  • 空间复杂度: O(n) - 哈希表存储n个节点,递归栈深度在最坏情况下为O(n)

4. 代码实现

4.1 方法一:基础递归法(易懂但效率低)

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)
 * }
 */
var buildTree = function(preorder, inorder) {
    if (preorder.length === 0 || inorder.length === 0) {
        return null;
    }
    
    // 根节点是前序遍历的第一个元素
    const rootVal = preorder[0];
    const root = new TreeNode(rootVal);
    
    // 在中序遍历中找到根节点的位置
    const rootIndexInInorder = inorder.indexOf(rootVal);
    
    // 分割中序遍历数组
    const leftInorder = inorder.slice(0, rootIndexInInorder);
    const rightInorder = inorder.slice(rootIndexInorder + 1);
    
    // 分割前序遍历数组
    const leftPreorder = preorder.slice(1, 1 + leftInorder.length);
    const rightPreorder = preorder.slice(1 + leftInorder.length);
    
    // 递归构建左右子树
    root.left = buildTree(leftPreorder, leftInorder);
    root.right = buildTree(rightPreorder, rightInorder);
    
    return root;
};

4.2 方法二:优化递归法(哈希表+索引)

javascript 复制代码
var buildTree = function(preorder, inorder) {
    // 创建中序值到索引的映射,提高查找效率
    const inorderMap = new Map();
    for (let i = 0; i < inorder.length; i++) {
        inorderMap.set(inorder[i], i);
    }
    
    // 递归构建函数
    const build = (preStart, preEnd, inStart, inEnd) => {
        // 递归终止条件
        if (preStart > preEnd || inStart > inEnd) {
            return null;
        }
        
        // 前序遍历的第一个节点是根节点
        const rootVal = preorder[preStart];
        const root = new TreeNode(rootVal);
        
        // 在中序遍历中找到根节点的位置
        const rootIndexInInorder = inorderMap.get(rootVal);
        
        // 计算左子树的节点数量
        const leftTreeSize = rootIndexInInorder - inStart;
        
        // 递归构建左子树
        // 前序遍历中左子树的范围: [preStart + 1, preStart + leftTreeSize]
        // 中序遍历中左子树的范围: [inStart, rootIndexInInorder - 1]
        root.left = build(
            preStart + 1, 
            preStart + leftTreeSize, 
            inStart, 
            rootIndexInInorder - 1
        );
        
        // 递归构建右子树
        // 前序遍历中右子树的范围: [preStart + leftTreeSize + 1, preEnd]
        // 中序遍历中右子树的范围: [rootIndexInInorder + 1, inEnd]
        root.right = build(
            preStart + leftTreeSize + 1, 
            preEnd, 
            rootIndexInInorder + 1, 
            inEnd
        );
        
        return root;
    };
    
    return build(0, preorder.length - 1, 0, inorder.length - 1);
};

4.3 方法三:迭代法(栈模拟)

javascript 复制代码
var buildTree = function(preorder, inorder) {
    if (preorder.length === 0) return null;
    
    const root = new TreeNode(preorder[0]);
    const stack = [root];
    let inorderIndex = 0;
    
    for (let i = 1; i < preorder.length; i++) {
        const preorderVal = preorder[i];
        let node = stack[stack.length - 1];
        
        // 如果栈顶节点的值不等于中序遍历当前值,说明当前节点是左子节点
        if (node.val !== inorder[inorderIndex]) {
            node.left = new TreeNode(preorderVal);
            stack.push(node.left);
        } else {
            // 否则,弹出所有与中序遍历匹配的节点
            while (stack.length > 0 && stack[stack.length - 1].val === inorder[inorderIndex]) {
                node = stack.pop();
                inorderIndex++;
            }
            // 当前节点是最后一个弹出节点的右子节点
            node.right = new TreeNode(preorderVal);
            stack.push(node.right);
        }
    }
    
    return root;
};

5. 各实现思路的复杂度、优缺点对比

方法 时间复杂度 空间复杂度 优点 缺点 适用场景
基础递归法 O(n²) O(n²) 代码直观,易于理解 每次查找根节点位置需要O(n),且数组切片产生额外空间开销 小规模数据,学习理解
优化递归法(哈希表+索引) O(n) O(n) 效率高,只遍历一次,无数组复制 需要额外哈希表空间,递归栈深度可能较大 生产环境,性能要求高
迭代法(栈) O(n) O(n) 避免递归栈溢出,空间效率高 逻辑相对复杂,不易理解 深度较大的树,避免递归深度限制

6. 总结

6.1 算法要点回顾

  1. 核心思想: 利用前序遍历确定根节点,中序遍历确定左右子树边界
  2. 关键优化: 使用哈希表存储中序索引,避免重复查找
  3. 实现技巧: 使用索引范围代替数组切片,减少空间开销

6.2 前端应用场景

  1. 虚拟DOM重建: 当需要根据前后状态差异重建DOM树时,类似的思想可以用于优化更新过程
  2. 配置文件解析: 解析嵌套的配置文件(如JSON Schema)构建配置对象树
  3. 路由权限树: 根据用户权限动态构建可访问的路由菜单树
  4. 组件树序列化: 将复杂的组件状态序列化存储,然后反序列化重建
  5. 可视化编辑器: 在图形化编辑器中构建和操作节点树
相关推荐
重生之我是Java开发战士2 小时前
【数据结构】Java对象的比较
java·jvm·数据结构
DanyHope2 小时前
LeetCode 206. 反转链表:迭代 + 递归双解法全解析
算法·leetcode·链表·递归·迭代
NAGNIP2 小时前
才发现TensorBoard是个可视化的神器!
算法
历程里程碑2 小时前
C++ 16:C++11新特化
c语言·开发语言·数据结构·c++·经验分享
_dindong2 小时前
算法杂谈:回溯路线
数据结构·算法·动态规划·bfs·宽度优先
咋吃都不胖lyh2 小时前
详解 UCB 算法的置信区间与核心逻辑(通俗 + 公式 + 实例)
人工智能·算法·机器学习
DanyHope2 小时前
LeetCode 两数之和:从 O (n²) 到 O (n),空间换时间的经典实践
前端·javascript·算法·leetcode·职场和发展
free-elcmacom2 小时前
机器学习高阶教程<6>推荐系统高阶修炼手册:混排、多任务与在线学习,解锁精准推荐新境界
人工智能·python·学习·算法·机器学习·机器人
断剑zou天涯2 小时前
【算法笔记】AC自动机
java·笔记·算法