【LeetCode Hot100 刷题日记 (50/100)】124. 二叉树中的最大路径和 —— 后序遍历 + 贪心剪枝🌳

3 阅读6分钟

📌 题目链接:124. 二叉树中的最大路径和 - 力扣(LeetCode)

🔍 难度:困难 | 🏷️ 标签:树、深度优先搜索(DFS)、递归、后序遍历

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

💾 空间复杂度:O(H) (H 为树的高度,最坏 O(N),平均 O(log N))


💡 一句话核心思想
每条路径在某个“最高点”处汇聚左右子树,我们通过后序遍历枚举所有可能的“最高点”,并用贪心思想决定是否将子树纳入路径。


🌳 题目分析

题目要求:在任意二叉树中,找出一条路径(节点不重复),使得路径上所有节点值之和最大

⚠️ 关键细节:

  • 路径 不必经过根节点
  • 路径 至少包含一个节点
  • 同一节点 不能重复出现(即路径是简单路径);
  • 路径可以 向左、向右、或只走单边,但一旦分叉(同时走左右子树),就不能再向上连接父节点。

🎯 目标:不是求从根出发的最大路径,而是全局任意位置的最大路径和


🔍 核心算法及代码讲解

✅ 核心思想:后序遍历 + 最大贡献值(Max Gain)

我们定义一个辅助函数 maxGain(node),其含义是:

node 为起点,向下延伸(只能走左或右,不能分叉)所能获得的最大路径和(称为“最大贡献值”)

这个值用于两个目的:

  1. 构建当前子树内部的完整路径(左 + node + 右);
  2. 向上返回给父节点,告诉它:“如果你要接我,最多能拿到多少收益”。

📌 为什么用后序遍历?

因为要先知道左右子树的“最大贡献值”,才能决定当前节点的路径组合和向上返回值。自底向上计算,天然适合后序遍历(左 → 右 → 根)。

🧩 关键逻辑拆解

int leftGain = max(maxGain(node->left), 0);
int rightGain = max(maxGain(node->right), 0);
  • 子树贡献若为负,不如不要(直接取 0),这是贪心剪枝的核心。
  • 因为加上负数只会让总和变小。
int priceNewpath = node->val + leftGain + rightGain;
maxSum = max(maxSum, priceNewpath);
  • 这是在以当前节点为“路径顶点” (即路径在此处拐弯)时,能形成的最大路径和。
  • 我们用全局变量 maxSum 记录所有可能中的最大值。
return node->val + max(leftGain, rightGain);
  • 向上返回时,只能选择左或右一条路(不能分叉),所以取 max(left, right)
  • 这保证了返回的是一条可向上延伸的单向路径

🧭 解题思路(分步详解)

  1. 初始化全局最大值 maxSum = INT_MIN,确保能覆盖全负数情况。

  2. 递归遍历整棵树(后序):

    • 对每个节点,先递归获取左右子树的最大正向贡献(负则舍弃)。
    • 计算以当前节点为顶点的路径和(左 + 当前 + 右),更新全局最大值。
    • 返回当前节点能向上提供的最大单边路径和(当前值 + max(左, 右))。
  3. 最终返回 maxSum,即全局最大路径和。

特别注意:即使所有节点都是负数,也必须选一个(题目要求至少一个节点),所以不能返回 0!


📊 算法分析

项目分析
时间复杂度O(N) :每个节点访问一次,常数操作。
空间复杂度O(H) :递归栈深度 = 树高 H。最坏退化为链表时 H = N;平衡树时 H = log N。
是否修改原树❌ 否,纯读取。
适用场景任意二叉树(含负权值),求最大路径和。
面试高频点✅ 路径定义理解、后序遍历应用、贪心剪枝、全局状态维护。

💻 代码

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:
    int maxSum = INT_MIN; // 全局最大路径和,初始化为最小整数

public:
    // 辅助函数:计算以 node 为起点的最大单边路径和(可向上延伸)
    int maxGain(TreeNode* node) {
        if (node == nullptr) {
            return 0; // 空节点贡献为 0
        }
        
        // 递归计算左右子树的最大贡献值,负数则舍弃(取 0)
        int leftGain = max(maxGain(node->left), 0);
        int rightGain = max(maxGain(node->right), 0);

        // 当前节点作为“路径顶点”时的路径和:左 + 当前 + 右
        int priceNewpath = node->val + leftGain + rightGain;

        // 更新全局最大路径和
        maxSum = max(maxSum, priceNewpath);

        // 返回当前节点能向上提供的最大单边路径和(只能选一边)
        return node->val + max(leftGain, rightGain);
    }

    int maxPathSum(TreeNode* root) {
        maxGain(root); // 启动递归
        return maxSum;
    }
};

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

    // 示例 1: [1,2,3]
    TreeNode* root1 = new TreeNode(1);
    root1->left = new TreeNode(2);
    root1->right = new TreeNode(3);
    Solution sol;
    cout << "Example 1: " << sol.maxPathSum(root1) << "\n"; // 输出: 6

    // 示例 2: [-10,9,20,null,null,15,7]
    TreeNode* root2 = new TreeNode(-10);
    root2->left = new TreeNode(9);
    root2->right = new TreeNode(20);
    root2->right->left = new TreeNode(15);
    root2->right->right = new TreeNode(7);
    cout << "Example 2: " << sol.maxPathSum(root2) << "\n"; // 输出: 42

    // 全负测试: [-3]
    TreeNode* root3 = new TreeNode(-3);
    cout << "All negative: " << sol.maxPathSum(root3) << "\n"; // 输出: -3

    return 0;
}

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 {TreeNode} root
 * @return {number}
 */
var maxPathSum = function (root) {
  // 全局最大路径和,初始化为安全整数最小值
  let maxSum = Number.MIN_SAFE_INTEGER;

  /**
   * 计算以 root 为起点的最大单边路径和(可向上延伸)
   * @param {TreeNode | null} node
   * @returns {number}
   */
  const maxGain = (node) => {
    if (node === null) return 0;

    // 递归获取左右子树的最大贡献,负数则舍弃(取 0)
    const leftGain = Math.max(maxGain(node.left), 0);
    const rightGain = Math.max(maxGain(node.right), 0);

    // 当前节点作为路径顶点的路径和
    const priceNewpath = node.val + leftGain + rightGain;

    // 更新全局最大值
    maxSum = Math.max(maxSum, priceNewpath);

    // 返回向上可延伸的最大单边路径和
    return node.val + Math.max(leftGain, rightGain);
  };

  maxGain(root);
  return maxSum;
};

💬 面试加分点

  1. 明确路径定义:强调“路径不能分叉后又向上”,这是理解为何返回单边值的关键。

  2. 处理全负数情况:很多初学者会错误地返回 0,需指出题目要求“至少一个节点”。

  3. 空间优化意识:虽然递归用了 O(H) 栈空间,但无法用迭代完全避免(因需回溯左右结果)。

  4. 扩展思考

    • 如果路径必须经过根节点?→ 简化为 root.val + maxLeft + maxRight
    • 如果允许重复访问?→ 变成图问题,完全不同。
    • 如果求最长路径(节点数最多)?→ 类似思路,但统计节点数而非求和。

🌟 本期完结,下期见!🔥

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

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

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