【前端算法】二叉树必刷8道高频面试题(最优解法+超详细解析)

17 阅读11分钟

【前端算法】二叉树必刷8道高频面试题(最优解法+超详细解析)

二叉树是前端面试中的必考知识点,本文将8道最经典、最高频的题目一网打尽。所有解法均为最优解,包含递归+迭代双思路、核心原理、复杂度分析、易错点提醒,面试直接背,新手也能轻松吃透!

一、二叉树节点定义(所有题目通用)

class TreeNode {
  constructor(val) {
    this.val = val;
    this.left = null; // 默认空节点,无需手动设置null
    this.right = null;
  }
}

二、层序遍历(BFS)- 二叉树基础核心

题目描述

从上到下、一层一层遍历二叉树,返回每层节点值的集合(按层分组)。

最优解法(队列实现)

const levelOrder = (root) => {
  const res = [];
  if (!root) return res; // 空树直接返回空数组

  const queue = [root]; // 队列存储待遍历节点(BFS核心)
  while (queue.length) {
    const levelSize = queue.length; // 关键:记录当前层节点数量(避免后续入队影响判断)
    const currentLevel = []; // 存储当前层节点值

    // 遍历当前层所有节点
    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift(); // 队首出队
      currentLevel.push(node.val);

      // 左孩子存在则入队(下一层节点)
      if (node.left) queue.push(node.left);
      // 右孩子存在则入队(下一层节点)
      if (node.right) queue.push(node.right);
    }

    res.push(currentLevel); // 保存当前层结果
  }
  return res;
};

核心解析

  • 数据结构:队列(先进先出,保证“一层一层”遍历);
  • 关键细节:必须先记录当前层长度(levelSize),再循环遍历,避免后续子节点入队导致长度变化,影响遍历范围;
  • 复杂度:时间O(n)(每个节点仅入队、出队一次),空间O(n)(队列最多存储一层节点);
  • 关联考点:掌握层序遍历,就能轻松写出“最大深度”的迭代解法(本质是统计层序遍历的总层数)。

三、二叉树最大深度

题目描述

求二叉树的最大深度(从根节点到最远叶子节点的最长路径上的节点数)。

方法1:递归法(DFS·自底向上,最简写法)

const maxDepth = (root) => {
  // 递归终止条件:空节点深度为0
  if (!root) return 0;
  // 递推公式:当前节点深度 = 1(自身) + 左右子树最大深度
  return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
};

原理:利用系统自动维护的调用栈,先递归到最底层(叶子节点),再自底向上回溯计算每一层的深度。

方法2:迭代法(BFS·自顶向下,无栈溢出风险)

const maxDepth = (root) => {
  if (!root) return 0;
  const queue = [root];
  let depth = 0; // 记录深度(层数)

  while (queue.length) {
    const levelSize = queue.length;
    // 遍历当前层所有节点
    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift();
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
    depth++; // 每处理完一层,深度+1
  }
  return depth;
};

核心总结(面试必说)

  • 递归DFS:依赖系统栈,自底向上计算,代码简洁,但树极深时会栈溢出;
  • 迭代BFS:依赖手动队列,自顶向下计数,代码稍长,但100%安全,永不爆栈;
  • 二者时间、空间复杂度均为O(n),目标一致,仅遍历方式和实现逻辑不同。

四、对称二叉树

题目描述

判断一棵二叉树是否是镜像对称的(即左右两边完全镜像,轴对称)。

最优递归解法

const isSymmetric = (root) => {
  // 辅助函数:判断两个节点是否镜像
  const check = (l, r) => {
    if (!l && !r) return true; // 两个都为空,对称
    if (!l || !r) return false; // 一个空一个非空,不对称
    // 核心:当前值相等 + 左的左 = 右的右 + 左的右 = 右的左
    return l.val === r.val && check(l.left, r.right) && check(l.right, r.left);
  };
  // 空树对称,非空树判断左右子树是否镜像
  return root ? check(root.left, root.right) : true;
};

核心思想

对称的核心是“镜像匹配”:左子树的左节点 = 右子树的右节点,左子树的右节点 = 右子树的左节点,递归校验每一对节点即可。

五、翻转二叉树

题目描述

将二叉树左右翻转(即镜像翻转,每个节点的左右孩子互换位置)。

最优极简解法(递归)

const invertTree = (root) => {
  if (!root) return null; // 空树直接返回
  // ES6解构赋值,一行交换左右子树(递归翻转左右子树后互换)
  [root.left, root.right] = [invertTree(root.right), invertTree(root.left)];
  return root;
};

补充:迭代解法(手动栈,无爆栈)

const invertTree = (root) => {
  if (!root) return null;
  const stack = [root];
  while (stack.length) {
    const node = stack.pop();
    // 交换当前节点的左右孩子
    [node.left, node.right] = [node.right, node.left];
    // 右孩子先入栈(保证左孩子先被处理,遍历顺序和递归一致)
    if (node.right) stack.push(node.right);
    if (node.left) stack.push(node.left);
  }
  return root;
};

六、验证二叉搜索树(BST)

题目描述

判断一棵二叉树是否为合法的二叉搜索树(BST),要求:左子树所有节点值 < 根节点值,右子树所有节点值 > 根节点值,且左右子树也必须是BST。

最优区间解法(递归,避免陷阱)

const isValidBST = (root) => {
  // 辅助函数:判断节点是否在合法区间内(min, max)
  const dfs = (node, min, max) => {
    if (!node) return true; // 空树是BST
    // 节点值必须严格大于min、小于max(避免等于,BST不允许重复值)
    if (node.val <= min || node.val >= max) return false;
    // 左子树:区间更新为(min, 当前节点值)
    // 右子树:区间更新为(当前节点值, max)
    return dfs(node.left, min, node.val) && dfs(node.right, node.val, max);
  };
  // 初始区间:负无穷到正无穷(根节点无约束)
  return dfs(root, -Infinity, Infinity);
};

易错提醒

不能只判断“当前节点 > 左孩子、当前节点 < 右孩子”,必须保证“左子树所有节点 < 根、右子树所有节点 > 根”,区间法是最简洁、最不易错的解法。

七、路径总和

题目描述

给定二叉树和一个目标和targetSum,判断是否存在一条从根节点到叶子节点的路径,路径上所有节点值相加等于目标和。(叶子节点:无左右孩子的节点)

方法1:递归解法(简洁,易理解)

const hasPathSum = function(root, targetSum) {
  if (!root) return false; // 空树无路径,返回false
  // 到达叶子节点,判断当前节点值是否等于剩余目标和
  if (!root.left && !root.right) {
    return root.val === targetSum;
  }
  // 递归左右子树,目标和减去当前节点值(剩余目标和传递)
  return hasPathSum(root.left, targetSum - root.val) 
      || hasPathSum(root.right, targetSum - root.val);
};

方法2:迭代解法(手动栈,无栈溢出)

const hasPathSum = function(root, targetSum) {
  if (!root) return false;

  // 栈中存储:[当前节点, 剩余需要满足的目标和]
  const stack = [[root, targetSum]];

  while (stack.length) {
    const [node, curSum] = stack.pop(); // DFS深度优先,栈顶出栈

    // 到达叶子节点,判断是否满足条件
    if (!node.left && !node.right) {
      if (node.val === curSum) {
        return true;
      }
    }

    // 右孩子先入栈(保证左孩子先被处理,和递归遍历顺序一致)
    if (node.right) {
      stack.push([node.right, curSum - node.val]);
    }
    if (node.left) {
      stack.push([node.left, curSum - node.val]);
    }
  }

  // 遍历完所有路径,均不满足条件
  return false;
};

核心总结

递归与迭代逻辑完全一致:都是“从根到叶子”遍历,传递剩余目标和,到达叶子节点时校验是否满足条件;区别仅在于“系统栈”和“手动栈”的管理方式。

八、二叉树的最近公共祖先(LCA)- 进阶压轴题

题目描述

给定一棵二叉树,找到两个指定节点p、q的最近公共祖先。最近公共祖先定义:同时包含p和q作为后代(p、q可互为祖先),且离p、q最近的节点。

为什么LCA是进阶题?(面试必懂)

LCA是二叉树最难的高频题,核心原因的是它的思维难度远超基础题,是二叉树递归的“分水岭”:

  1. 递归逻辑反直觉:不直接判断当前节点,而是先让左右子树“找p、q”,再根据子树的返回结果,推导当前节点是否是LCA,属于“后序遍历+信息向上传递+全局决策”的高阶思维;
  2. 代码短但难理解:核心逻辑仅2行,但新手很难想明白“为什么左右子树各找到一个,当前节点就是LCA”;
  3. 全局判断需求:不同于基础题的“局部判断”,LCA需要掌握整棵树中p、q的位置,依赖子树的返回信息才能做出决策;
  4. 迭代法复杂:基础题迭代多是“直接模拟递归”,而LCA迭代需要记录父节点、回溯祖先路径,步骤更繁琐。

方法1:递归解法(最优最简,面试首选)

var lowestCommonAncestor = function(root, p, q) {
  // 递归终止条件:空节点/找到p/找到q,直接返回(无需继续递归)
  if (!root || root === p || root === q) return root;

  // 去左子树找p、q
  const left = lowestCommonAncestor(root.left, p, q);
  // 去右子树找p、q
  const right = lowestCommonAncestor(root.right, p, q);

  // 左右子树各找到一个 → 当前节点就是LCA(最近公共祖先)
  if (left && right) return root;
  // 只有一边找到 → 答案在找到的那一边(递归回溯)
  return left || right;
};

方法2:迭代解法(手动栈+父指针,无栈溢出)

var lowestCommonAncestor = function(root, p, q) {
  if (!root) return null;

  // 1. 用栈做DFS遍历,记录每个节点的父节点(核心:给每个节点“找爸爸”)
  const stack = [root];
  const parent = new Map(); // key=子节点,value=父节点
  parent.set(root, null); // 根节点无父节点

  // 第一步:遍历整棵树,完善父节点映射
  while (stack.length) {
    const node = stack.pop();

    // 右孩子入栈,记录父节点
    if (node.right) {
      parent.set(node.right, node);
      stack.push(node.right);
    }
    // 左孩子入栈,记录父节点
    if (node.left) {
      parent.set(node.left, node);
      stack.push(node.left);
    }
  }

  // 第二步:收集p的所有祖先(从p往上走到根)
  const ancestors = new Set();
  while (p) {
    ancestors.add(p);
    p = parent.get(p); // 向上找父节点
  }

  // 第三步:q往上走,第一个出现在p祖先集合中的节点,就是LCA
  while (!ancestors.has(q)) {
    q = parent.get(q);
  }

  return q;
};

核心总结

  • 递归LCA:代码短、效率高(O(n)),但难理解,深度过深会栈溢出;
  • 迭代LCA:思路直观(找父节点+回溯祖先),永不爆栈,工程中更常用,面试写这个能加分;
  • 二者时间、空间复杂度均为O(n),都是最优解,可根据面试场景选择(递归显简洁,迭代显功底)。

九、补充1:递归栈溢出的解决方案(面试必问)

1. 为什么递归会栈溢出?

系统调用栈的空间非常小(通常仅1MB左右),每递归一次,就会向栈中压入一个“栈帧”;当递归深度超过栈的容量(比如树深度达到1000+),就会报错 RangeError: Maximum call stack size exceeded

2. 终极解决方案(3种,优先选第一种)

  1. 改用迭代法(最稳、最通用) :放弃递归,手动维护栈(DFS)或队列(BFS),无论树多深,都不会栈溢出(本文所有递归题均提供了对应的迭代解法);
  2. 尾递归优化(理论有用,JS几乎无效) :让递归调用成为函数的最后一步,理论上可避免栈帧累积,但Chrome、Node.js等主流JS环境均不支持尾递归优化,写了也会爆栈;
  3. 限制递归深度(工程不推荐) :手动设置递归深度阈值,超过阈值则停止递归,适合简单场景,不适合算法题。

面试标准答案(直接背)

面试官问:“递归会栈溢出吗?怎么解决?” 回答:递归深度过深会导致系统调用栈溢出。解决方法是放弃递归,改用迭代实现,手动维护栈(DFS)或队列(BFS),这样无论数据规模多大,都不会栈溢出。

十、补充2:LeetCode 数组转二叉树原理(新手必懂)

疑问:为什么LeetCode输入是数组,我们却能用 .left、.right?

真相:LeetCode界面上的数组,只是“便于人类阅读和输入”的树的序列化表示,后台会自动将数组转换为TreeNode对象,传给我们函数的root,已经是带left、right属性的树节点了,我们无需手动处理数组。

LeetCode官方数组转树实现(可直接运行)

function arrayToTree(arr) {
  if (!arr || arr.length === 0) return null;

  // 根节点:数组第0位
  const root = new TreeNode(arr[0]);
  // 队列:存储待挂载孩子的节点(BFS构建)
  const queue = [root];
  let i = 1; // 从第1位开始,分配左右孩子

  while (queue.length && i < arr.length) {
    const parent = queue.shift(); // 取出父节点

    // 左孩子:下标i,不为null则创建节点并挂载
    if (arr[i] !== null) {
      parent.left = new TreeNode(arr[i]);
      queue.push(parent.left); // 左孩子入队,后续给它分配孩子
    }
    i++; // 无论是否有左孩子,都移动到下一位(右孩子)

    // 右孩子:下标i,不为null则创建节点并挂载
    if (i < arr.length && arr[i] !== null) {
      parent.right = new TreeNode(arr[i]);
      queue.push(parent.right); // 右孩子入队
    }
    i++; // 移动到下一组左右孩子
  }
  return root;
}

关键说明

  • 转换规则:数组第0位是根节点,下标i的左孩子是2i+1,右孩子是2i+2,null表示该位置无节点;
  • 无需手动设置null:TreeNode默认left、right为null,当数组中是null时,不处理即可,保持默认值;
  • 核心逻辑:用BFS一层一层构建,先有父节点,再依次挂载左右孩子。

十一、最终总结(面试必背)

  1. 遍历逻辑:层序遍历=BFS=队列=自顶向下;递归=DFS=系统栈=自底向上;
  2. 递归vs迭代:递归简洁好写,但有栈溢出风险;迭代代码稍长,但100%安全,工程首选;
  3. 重点题目:最大深度(递归+迭代双思路)、LCA(进阶压轴,递归+迭代必掌握)、路径总和(递归与迭代逻辑一致);
  4. 易错点:验证BST用区间法,避免局部判断陷阱;数组转树无需手动设null,TreeNode默认空;
  5. LeetCode技巧:输入数组是展示用,后台自动转树,直接用.left、.right即可。

本文涵盖二叉树所有前端面试高频考点,建议收藏背诵,面试时直接套用解法,轻松拿下二叉树题目!欢迎点赞关注,后续持续更新前端面试高频算法题~