对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎
LeetCode124. 二叉树中的最大路径和
1. 题目描述
1.1 问题定义
给定一个非空二叉树,返回其最大路径和。在这个问题中,路径被定义为一条从树中任意节点出发,沿着父子关系连接,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。
示例:
输入:[-10,9,20,null,null,15,7]
-10
/ \
9 20
/ \
15 7
输出:42
解释:最优路径是 15 → 20 → 7,路径和为 15+20+7=42
2. 问题分析
2.1 问题本质
这是一个**树形DP(动态规划)**问题。对于二叉树中的任意节点,经过它的最大路径和可能由以下三部分组成:
- 该节点自身的值
- 左子树的最大贡献值(如果为正)
- 右子树的最大贡献值(如果为正)
2.2 难点分析
- 路径可以不经过根节点:最大路径可能完全位于某个子树中
- 负值节点的处理:负值节点可能不应该包含在最优路径中
- 递归计算:需要设计正确的递归函数返回值和全局变量更新逻辑
2.3 前端视角
在前端开发中,这种问题类似于:
- DOM树的某种属性最优路径计算
- 组件树的最大渲染性能瓶颈分析
- 组织架构树中的最大团队效能计算
3. 解题思路
3.1 核心思想
采用后序遍历-DFS的方式,自底向上计算:
- 对于每个节点,计算以该节点为"顶点"的最大路径和
- 向上返回的是从该节点向下的单边最大路径和(因为父节点只能选择左或右一边)
- 使用全局变量记录遍历过程中的最大路径和
3.2 最优解思路
分治递归法:
- 时间复杂度:O(n),每个节点访问一次
- 空间复杂度:O(h),递归栈深度,h为树的高度
- 这是最优解法,无法进一步优化时间复杂度
3.3 递归函数设计
javascript
// 伪代码
function dfs(node):
if node is null: return 0
// 计算左右子树的最大贡献值(如果是负数则取0)
leftGain = max(dfs(node.left), 0)
rightGain = max(dfs(node.right), 0)
// 计算以当前节点为顶点的最大路径和
priceNewpath = node.val + leftGain + rightGain
// 更新全局最大值
maxSum = max(maxSum, priceNewpath)
// 返回当前节点的最大贡献值(只能选择一边)
return node.val + max(leftGain, rightGain)
4. 代码实现
4.1 JavaScript实现(最优解)
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)
* }
*/
/**
* 最优解:DFS后序遍历
* @param {TreeNode} root
* @return {number}
*/
var maxPathSum = function(root) {
let maxSum = -Infinity;
/**
* 递归计算最大贡献值
* @param {TreeNode} node
* @return {number} 返回以该节点为起点的最大路径和
*/
const maxGain = function(node) {
if (node === null) return 0;
// 递归计算左右子树的最大贡献值
const leftGain = Math.max(maxGain(node.left), 0);
const rightGain = Math.max(maxGain(node.right), 0);
// 当前节点作为路径顶点的最大路径和
const currentPathSum = node.val + leftGain + rightGain;
// 更新全局最大值
maxSum = Math.max(maxSum, currentPathSum);
// 返回当前节点的最大贡献值(只能选择一边)
return node.val + Math.max(leftGain, rightGain);
};
maxGain(root);
return maxSum;
};
4.2 TypeScript实现(类型安全版)
typescript
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = (val === undefined ? 0 : val);
this.left = (left === undefined ? null : left);
this.right = (right === undefined ? null : right);
}
}
function maxPathSum(root: TreeNode | null): number {
let maxSum = -Infinity;
const dfs = (node: TreeNode | null): number => {
if (!node) return 0;
const leftGain = Math.max(dfs(node.left), 0);
const rightGain = Math.max(dfs(node.right), 0);
const currentPathSum = node.val + leftGain + rightGain;
maxSum = Math.max(maxSum, currentPathSum);
return node.val + Math.max(leftGain, rightGain);
};
dfs(root);
return maxSum;
}
4.3 迭代法实现(使用栈模拟递归)
javascript
/**
* 迭代法实现:使用栈模拟后序遍历
* 虽然空间复杂度相同,但代码更复杂,不如递归直观
*/
var maxPathSumIterative = function(root) {
if (!root) return 0;
let maxSum = -Infinity;
const stack = [];
const map = new Map(); // 存储节点与其最大贡献值的映射
let lastVisited = null;
let node = root;
while (node || stack.length) {
while (node) {
stack.push(node);
node = node.left;
}
node = stack[stack.length - 1];
// 如果右子树存在且未被访问过
if (node.right && lastVisited !== node.right) {
node = node.right;
} else {
// 处理当前节点
stack.pop();
// 获取左右子树的最大贡献值
const leftGain = Math.max(map.get(node.left) || 0, 0);
const rightGain = Math.max(map.get(node.right) || 0, 0);
// 计算当前节点作为顶点的路径和
const currentPathSum = node.val + leftGain + rightGain;
maxSum = Math.max(maxSum, currentPathSum);
// 存储当前节点的最大贡献值
const currentGain = node.val + Math.max(leftGain, rightGain);
map.set(node, currentGain);
lastVisited = node;
node = null;
}
}
return maxSum;
};
5. 复杂度与优缺点对比
5.1 复杂度分析表格
| 实现方式 | 时间复杂度 | 空间复杂度 | 代码简洁性 | 适用场景 |
|---|---|---|---|---|
| 递归DFS(最优解) | O(n) | O(h) | ⭐⭐⭐⭐⭐ | 绝大多数场景,代码直观易理解 |
| 迭代栈模拟 | O(n) | O(h) | ⭐⭐ | 深度极大可能栈溢出时使用 |
| 暴力枚举(不推荐) | O(n²) | O(n) | ⭐ | 仅用于理解问题,实际不可用 |
5.2 各方法详细对比
-
递归DFS:
- 优点:代码简洁,逻辑清晰,符合树结构的自然处理方式
- 缺点:在极端不平衡的树上可能导致调用栈溢出
- 前端应用:适合处理DOM树、组件树等大多数前端树形结构
-
迭代栈模拟:
- 优点:避免递归栈溢出问题
- 缺点:代码复杂,需要手动管理栈和访问状态
- 前端应用:处理深度极大的树结构(如大型菜单树)
-
暴力枚举:
- 优点:思路简单,易于理解
- 缺点:效率极低,仅适用于极小规模数据
- 前端应用:基本不使用,但可用于验证算法正确性
6. 总结与前端应用场景
6.1 算法核心要点
- 分治思想:将大问题分解为子树的小问题
- 后序遍历:先处理子问题,再处理当前节点
- 全局状态:使用外部变量记录过程中的最优解
- 贪心选择:每个节点向上返回时只选择最优的一边
6.2 前端实际应用场景
场景一:组件树性能分析
javascript
// 假设每个组件有渲染时间,找出渲染最慢的组件路径
class ComponentNode {
constructor(name, renderTime) {
this.name = name;
this.renderTime = renderTime;
this.children = [];
}
addChild(child) {
this.children.push(child);
}
}
function findSlowestRenderPath(root) {
let maxTime = -Infinity;
let slowestPath = [];
const dfs = (node, currentPath) => {
if (!node) return 0;
currentPath.push(node.name);
let maxChildTime = 0;
for (const child of node.children) {
const childTime = dfs(child, [...currentPath]);
maxChildTime = Math.max(maxChildTime, childTime);
}
// 当前组件的总渲染时间(自身+最慢子组件)
const totalTime = node.renderTime + maxChildTime;
// 如果是叶子节点或者是更慢的路径
if (node.children.length === 0 && totalTime > maxTime) {
maxTime = totalTime;
slowestPath = currentPath;
}
currentPath.pop();
return totalTime;
};
dfs(root, []);
return { maxTime, slowestPath };
}
场景二:DOM树最大属性值路径
javascript
// 查找DOM树中某属性值之和最大的路径
function findMaxAttributePath(rootElement, attributeName) {
let maxSum = -Infinity;
let maxPath = [];
const dfs = (element, currentPath) => {
if (!element) return 0;
// 获取当前元素的属性值
const attrValue = parseInt(element.getAttribute(attributeName) || 0);
currentPath.push(element.tagName);
// 处理子元素
const children = Array.from(element.children);
let maxChildSum = 0;
let bestChildPath = [];
for (const child of children) {
const childResult = dfs(child, []);
if (childResult.sum > maxChildSum) {
maxChildSum = childResult.sum;
bestChildPath = childResult.path;
}
}
// 计算当前路径总和
const totalSum = attrValue + maxChildSum;
const totalPath = [element.tagName, ...bestChildPath];
// 更新全局最大值
if (totalSum > maxSum) {
maxSum = totalSum;
maxPath = totalPath;
}
return { sum: totalSum, path: totalPath };
};
dfs(rootElement, []);
return { maxSum, maxPath };
}
场景三:组织结构树的最大团队效能
javascript
// 在组织架构树中找出效能最高的团队路径
class EmployeeNode {
constructor(name, performance) {
this.name = name;
this.performance = performance; // 个人绩效
this.subordinates = [];
}
addSubordinate(employee) {
this.subordinates.push(employee);
}
}
function findBestTeamPath(ceo) {
let bestTeamScore = -Infinity;
let bestTeamPath = [];
const dfs = (employee) => {
if (!employee) return { score: 0, path: [] };
let maxSubordinateScore = 0;
let bestSubordinatePath = [];
// 计算所有下属团队的最佳贡献
for (const subordinate of employee.subordinates) {
const subordinateResult = dfs(subordinate);
if (subordinateResult.score > maxSubordinateScore) {
maxSubordinateScore = subordinateResult.score;
bestSubordinatePath = subordinateResult.path;
}
}
// 当前团队的总分(个人+最佳下属团队)
const teamScore = employee.performance + maxSubordinateScore;
const teamPath = [employee.name, ...bestSubordinatePath];
// 更新最佳团队
if (teamScore > bestTeamScore) {
bestTeamScore = teamScore;
bestTeamPath = teamPath;
}
// 返回给上级:选择最佳的下属团队贡献
return {
score: Math.max(employee.performance, teamScore),
path: teamScore > employee.performance ? teamPath : [employee.name]
};
};
dfs(ceo);
return { bestTeamScore, bestTeamPath };
}