Skip to content

树算法题解题思路与代码分析

1. 树的遍历(基础)

核心解题思路

  • 深度优先遍历(DFS):利用递归或栈实现前序、中序、后序遍历。递归写法简洁,迭代写法需手动管理栈。
  • 广度优先遍历(BFS):利用队列实现层序遍历,可获取每层节点。

经典例题:102. 二叉树的层序遍历

题目:给你二叉树的根节点 root,返回其节点值的层序遍历结果(即逐层地,从左到右访问所有节点)。

javascript
/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function(root) {
    if (!root) return [];
    const result = [];
    const queue = [root]; // 队列初始包含根节点

    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);
        }
        result.push(currentLevel);
    }
    return result;
};

关键点

  • 使用队列进行 BFS,每次处理一层。
  • 循环开始时记录当前队列长度,确保只处理当前层的节点。
  • shift() 操作在 JavaScript 中时间复杂度 O(n),但 LeetCode 数据量下可接受;优化可用索引模拟队列。

经典例题:94. 二叉树的中序遍历(迭代版)

题目:给定一个二叉树的根节点 root,返回它的中序遍历。

javascript
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = function(root) {
    const result = [];
    const stack = [];
    let curr = root;

    while (curr || stack.length) {
        // 不断将左子树压栈
        while (curr) {
            stack.push(curr);
            curr = curr.left;
        }
        // 弹出栈顶,访问节点
        curr = stack.pop();
        result.push(curr.val);
        // 转向右子树
        curr = curr.right;
    }
    return result;
};

关键点

  • 模拟系统栈的过程:先深入左子树,访问根,再处理右子树。
  • 迭代写法避免了递归可能导致的栈溢出。

2. 树的性质与属性

核心解题思路

利用递归计算树的深度、对称性、平衡性等属性。通常定义递归函数返回所需信息,并在后序位置进行汇总。

经典例题:104. 二叉树的最大深度

题目:给定一个二叉树,找出其最大深度。

javascript
/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function(root) {
    if (!root) return 0;
    const leftDepth = maxDepth(root.left);
    const rightDepth = maxDepth(root.right);
    // 当前节点的深度 = 左右子树最大深度 + 1
    return Math.max(leftDepth, rightDepth) + 1;
};

关键点

  • 递归终止条件:空节点深度为 0。
  • 当前节点深度由子树深度决定,后序位置计算。

经典例题:101. 对称二叉树

题目:给定一个二叉树,检查它是否是镜像对称的。

javascript
/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isSymmetric = function(root) {
    if (!root) return true;
    return isMirror(root.left, root.right);
};

function isMirror(left, right) {
    if (!left && !right) return true; // 都为空,对称
    if (!left || !right) return false; // 一个为空,不对称
    // 节点值相等,且左树的左子树与右树的右子树对称,左树的右子树与右树的左子树对称
    return left.val === right.val &&
           isMirror(left.left, right.right) &&
           isMirror(left.right, right.left);
}

关键点

  • 定义辅助函数比较两个节点是否互为镜像。
  • 递归比较:外侧对内侧,内侧对外侧。

3. 二叉搜索树(BST)

核心解题思路

二叉搜索树的中序遍历是升序序列。利用这一特性可快速验证 BST、查找第 K 小元素等。此外,可借助上下界递归判断 BST 合法性。

经典例题:98. 验证二叉搜索树

题目:给你一个二叉树的根节点 root,判断其是否是一个有效的二叉搜索树。

javascript
/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isValidBST = function(root) {
    // 辅助函数,传递当前节点允许的取值范围 (min, max)
    function validate(node, min, max) {
        if (!node) return true;
        // 当前节点值必须在 (min, max) 范围内
        if (node.val <= min || node.val >= max) return false;
        // 左子树所有节点值必须小于当前节点值,即更新上界为 node.val
        // 右子树所有节点值必须大于当前节点值,即更新下界为 node.val
        return validate(node.left, min, node.val) &&
               validate(node.right, node.val, max);
    }
    return validate(root, -Infinity, Infinity);
};

关键点

  • 不能只比较当前节点与左右子节点的值,因为 BST 要求整棵左子树都小于根,整棵右子树都大于根。
  • 利用上下界递归传递约束条件。

经典例题:230. 二叉搜索树中第K小的元素

题目:给定一个二叉搜索树的根节点 root,和一个整数 k,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。

javascript
/**
 * @param {TreeNode} root
 * @param {number} k
 * @return {number}
 */
var kthSmallest = function(root, k) {
    const stack = [];
    let curr = root;
    let count = 0;

    while (curr || stack.length) {
        // 中序遍历:左 -> 根 -> 右
        while (curr) {
            stack.push(curr);
            curr = curr.left;
        }
        curr = stack.pop();
        count++;
        if (count === k) return curr.val;
        curr = curr.right;
    }
    return -1; // 不会执行
};

关键点

  • 利用 BST 中序遍历得到升序序列的特性。
  • 当遍历到第 k 个节点时直接返回其值,提前终止。

4. 树的路径与求和

核心解题思路

从根到叶子或任意节点之间的路径和问题,通常用 DFS 回溯解决。记录当前路径和,到达叶子时判断是否满足条件。对于路径总和 III,需要利用前缀和思想。

经典例题:112. 路径总和

题目:给你二叉树的根节点 root 和一个表示目标和的整数 targetSum,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。

javascript
/**
 * @param {TreeNode} root
 * @param {number} targetSum
 * @return {boolean}
 */
var hasPathSum = function(root, targetSum) {
    if (!root) return false;
    // 如果是叶子节点,检查是否等于 targetSum
    if (!root.left && !root.right) {
        return root.val === targetSum;
    }
    // 递归检查左右子树,目标和减去当前节点值
    return hasPathSum(root.left, targetSum - root.val) ||
           hasPathSum(root.right, targetSum - root.val);
};

关键点

  • 递归过程中不断减去当前节点值,到达叶子时判断是否归零。
  • 注意空树的情况。

经典例题:113. 路径总和 II

题目:给你二叉树的根节点 root 和一个整数目标和 targetSum,找出所有从根节点到叶子节点路径总和等于给定目标和的路径。

javascript
/**
 * @param {TreeNode} root
 * @param {number} targetSum
 * @return {number[][]}
 */
var pathSum = function(root, targetSum) {
    const result = [];
    const path = [];

    function dfs(node, remaining) {
        if (!node) return;
        path.push(node.val);
        // 到达叶子且剩余值等于当前节点值(即路径和等于 targetSum)
        if (!node.left && !node.right && remaining === node.val) {
            result.push([...path]); // 拷贝路径
        } else {
            dfs(node.left, remaining - node.val);
            dfs(node.right, remaining - node.val);
        }
        path.pop(); // 回溯
    }

    dfs(root, targetSum);
    return result;
};

关键点

  • 使用回溯法,在递归前将当前节点加入路径,递归后弹出。
  • 到达叶子且满足条件时,将当前路径的拷贝加入结果集。

5. 树的构造与序列化

核心解题思路

给定两种遍历序列(如前序+中序)可唯一确定一棵二叉树。递归构建:前序第一个为根,在中序中找到根的位置,分割左右子树,递归构建左右子树。

经典例题:105. 从前序与中序遍历序列构造二叉树

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

javascript
/**
 * @param {number[]} preorder
 * @param {number[]} inorder
 * @return {TreeNode}
 */
var buildTree = function(preorder, inorder) {
    // 构建中序遍历值到下标的映射,便于快速定位根的位置
    const map = new Map();
    for (let i = 0; i < inorder.length; i++) {
        map.set(inorder[i], i);
    }

    function build(preStart, preEnd, inStart, inEnd) {
        if (preStart > preEnd) return null;
        const rootVal = preorder[preStart];
        const root = new TreeNode(rootVal);
        const inIndex = map.get(rootVal); // 根在中序中的位置
        const leftSize = inIndex - inStart; // 左子树节点数

        // 左子树:前序 [preStart+1, preStart+leftSize],中序 [inStart, inIndex-1]
        root.left = build(preStart + 1, preStart + leftSize, inStart, inIndex - 1);
        // 右子树:前序 [preStart+leftSize+1, preEnd],中序 [inIndex+1, inEnd]
        root.right = build(preStart + leftSize + 1, preEnd, inIndex + 1, inEnd);

        return root;
    }

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

关键点

  • 利用哈希表快速获取根节点在中序遍历中的位置。
  • 确定左右子树的节点数量,从而划分前序遍历数组的区间。
  • 递归构建左右子树。

6. 最近公共祖先(LCA)

核心解题思路

递归查找:若当前节点为空或等于 p 或 q,则返回当前节点。否则递归查找左右子树。如果左右子树均找到非空节点,则当前节点为 LCA;否则返回非空的那一侧。

经典例题:236. 二叉树的最近公共祖先

题目:给定一个二叉树,找到该树中两个指定节点的最近公共祖先。

javascript
/**
 * @param {TreeNode} root
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {TreeNode}
 */
var lowestCommonAncestor = function(root, p, q) {
    if (!root || root === p || root === q) return root;
    const left = lowestCommonAncestor(root.left, p, q);
    const right = lowestCommonAncestor(root.right, p, q);
    // 如果左右子树分别找到了 p 和 q,则当前节点是 LCA
    if (left && right) return root;
    // 否则返回找到的那一侧
    return left || right;
};

关键点

  • 递归返回值的含义:如果找到了 p 或 q,或者找到了它们的祖先,则返回该节点;否则返回 null。
  • 后序遍历位置处理左右子树的结果。

7. 层序遍历变种

核心解题思路

在 BFS 层序遍历的基础上,针对每层记录所需信息(如最后一个节点、最大值、平均值等)。

经典例题:199. 二叉树的右视图

题目:给定一个二叉树的根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

javascript
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var rightSideView = function(root) {
    if (!root) return [];
    const result = [];
    const queue = [root];

    while (queue.length) {
        const levelSize = queue.length;
        for (let i = 0; i < levelSize; i++) {
            const node = queue.shift();
            // 如果是当前层的最后一个节点,将其值加入结果
            if (i === levelSize - 1) result.push(node.val);
            if (node.left) queue.push(node.left);
            if (node.right) queue.push(node.right);
        }
    }
    return result;
};

关键点

  • 层序遍历时,记录每层的最后一个节点即可。
  • 也可使用 DFS 先右后左,记录每层第一个访问的节点。

8. 其他进阶问题

核心解题思路

这类题目涉及更复杂的树结构或巧妙的算法思想,如前序遍历展开、填充 next 指针、树形 DP 等。

经典例题:114. 二叉树展开为链表

题目:给你二叉树的根节点 root,请你将它展开为一个单链表:展开后的单链表应该同样使用 TreeNode,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null。展开顺序与二叉树的前序遍历顺序相同。

javascript
/**
 * @param {TreeNode} root
 * @return {void} Do not return anything, modify root in-place instead.
 */
var flatten = function(root) {
    if (!root) return;
    // 后序遍历方式展开
    flatten(root.left);
    flatten(root.right);
    // 暂存右子树
    const right = root.right;
    // 将左子树移到右边
    root.right = root.left;
    root.left = null;
    // 找到当前右子树的末端,接上原来的右子树
    let curr = root;
    while (curr.right) {
        curr = curr.right;
    }
    curr.right = right;
};

关键点

  • 后序遍历顺序:左子树展开、右子树展开,然后将左子树插入到根和右子树之间。
  • 需要找到当前右子树的最后一个节点,以便连接原来的右子树。

总结

以上八个分类基本覆盖了力扣上树相关的主流题型。建议按照以下顺序练习:

  1. 基础遍历102. 层序遍历94. 中序遍历
  2. 树的性质104. 最大深度101. 对称二叉树
  3. 二叉搜索树98. 验证 BST230. 第K小元素
  4. 路径与求和112. 路径总和113. 路径总和 II
  5. 构造与序列化105. 从前序中序构造二叉树
  6. 最近公共祖先236. 二叉树的最近公共祖先
  7. 层序遍历变种199. 右视图
  8. 进阶挑战114. 展开为链表