📌 题目链接:124. 二叉树中的最大路径和 - 力扣(LeetCode)
🔍 难度:困难 | 🏷️ 标签:树、深度优先搜索(DFS)、递归、后序遍历
⏱️ 目标时间复杂度:O(N)
💾 空间复杂度:O(H) (H 为树的高度,最坏 O(N),平均 O(log N))
💡 一句话核心思想:
每条路径在某个“最高点”处汇聚左右子树,我们通过后序遍历枚举所有可能的“最高点”,并用贪心思想决定是否将子树纳入路径。
🌳 题目分析
题目要求:在任意二叉树中,找出一条路径(节点不重复),使得路径上所有节点值之和最大。
⚠️ 关键细节:
- 路径 不必经过根节点;
- 路径 至少包含一个节点;
- 同一节点 不能重复出现(即路径是简单路径);
- 路径可以 向左、向右、或只走单边,但一旦分叉(同时走左右子树),就不能再向上连接父节点。
🎯 目标:不是求从根出发的最大路径,而是全局任意位置的最大路径和。
🔍 核心算法及代码讲解
✅ 核心思想:后序遍历 + 最大贡献值(Max Gain)
我们定义一个辅助函数 maxGain(node),其含义是:
以
node为起点,向下延伸(只能走左或右,不能分叉)所能获得的最大路径和(称为“最大贡献值”) 。
这个值用于两个目的:
- 构建当前子树内部的完整路径(左 + node + 右);
- 向上返回给父节点,告诉它:“如果你要接我,最多能拿到多少收益”。
📌 为什么用后序遍历?
因为要先知道左右子树的“最大贡献值”,才能决定当前节点的路径组合和向上返回值。自底向上计算,天然适合后序遍历(左 → 右 → 根)。
🧩 关键逻辑拆解
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)。 - 这保证了返回的是一条可向上延伸的单向路径。
🧭 解题思路(分步详解)
-
初始化全局最大值
maxSum = INT_MIN,确保能覆盖全负数情况。 -
递归遍历整棵树(后序):
- 对每个节点,先递归获取左右子树的最大正向贡献(负则舍弃)。
- 计算以当前节点为顶点的路径和(左 + 当前 + 右),更新全局最大值。
- 返回当前节点能向上提供的最大单边路径和(当前值 + max(左, 右))。
-
最终返回
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;
};
💬 面试加分点
-
明确路径定义:强调“路径不能分叉后又向上”,这是理解为何返回单边值的关键。
-
处理全负数情况:很多初学者会错误地返回 0,需指出题目要求“至少一个节点”。
-
空间优化意识:虽然递归用了 O(H) 栈空间,但无法用迭代完全避免(因需回溯左右结果)。
-
扩展思考:
- 如果路径必须经过根节点?→ 简化为
root.val + maxLeft + maxRight。 - 如果允许重复访问?→ 变成图问题,完全不同。
- 如果求最长路径(节点数最多)?→ 类似思路,但统计节点数而非求和。
- 如果路径必须经过根节点?→ 简化为
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!