对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎
LeetCode 437. 路径总和 III
1. 题目描述
给定一个二叉树的根节点 root 和一个整数 targetSum,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
示例 1:
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有:
1. 5 → 3
2. 5 → 2 → 1
3. -3 → 11
示例 2:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:3
2. 问题分析
2.1 问题特点
- 路径定义灵活:路径可以从任意节点开始,到任意节点结束
- 方向限制:只能从上到下(父节点到子节点)
- 数值特点:节点值可能为负数,这增加了问题的复杂性
- 树结构:典型的二叉树遍历问题
2.2 前端视角理解
在前端开发中,类似的问题场景包括:
- DOM树遍历:查找符合特定条件的节点组合
- 组件树状态管理:统计组件树中满足特定状态条件的路径
- 路由权限验证:检查权限链是否符合特定规则
3. 解题思路
3.1 暴力递归法(DFS)
遍历每个节点,以该节点为起点向下搜索所有路径,检查路径和是否等于targetSum。
3.2 前缀和优化法(最优解)
利用前缀和思想,将路径和问题转化为"两数之差"问题,类似数组中的两数之和问题。
核心思想:
- 记录从根节点到当前节点的路径和(前缀和)
- 查找当前前缀和与targetSum的差值是否在历史前缀和中出现过
- 使用哈希表(Map)存储前缀和的出现次数
4. 代码实现
4.1 方法一:双重递归(暴力法)
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)
* }
*/
/**
* 暴力递归法
* 时间复杂度:O(n²),空间复杂度:O(h)
*/
var pathSum = function(root, targetSum) {
if (!root) return 0;
// 从当前节点开始的路径
const countFromNode = (node, sum) => {
if (!node) return 0;
let count = 0;
// 如果当前节点的值等于剩余和,找到一条路径
if (node.val === sum) {
count++;
}
// 继续向左右子树搜索
count += countFromNode(node.left, sum - node.val);
count += countFromNode(node.right, sum - node.val);
return count;
};
// 从当前节点开始的路径 + 从左子树开始的路径 + 从右子树开始的路径
return countFromNode(root, targetSum) +
pathSum(root.left, targetSum) +
pathSum(root.right, targetSum);
};
4.2 方法二:前缀和优化法(最优解)
javascript
/**
* 前缀和优化法 - 最优解
* 时间复杂度:O(n),空间复杂度:O(n)
*/
var pathSum = function(root, targetSum) {
// 使用Map存储前缀和及其出现次数
const prefixSumMap = new Map();
// 初始化:前缀和为0的路径有1条(空路径)
prefixSumMap.set(0, 1);
/**
* DFS遍历
* @param {TreeNode} node 当前节点
* @param {number} currentSum 当前路径和
* @return {number} 符合条件的路径数
*/
const dfs = (node, currentSum) => {
if (!node) return 0;
// 更新当前路径和
currentSum += node.val;
// 查找是否存在前缀和使得 currentSum - prefixSum = targetSum
// 即:prefixSum = currentSum - targetSum
const target = currentSum - targetSum;
let count = prefixSumMap.get(target) || 0;
// 将当前路径和加入哈希表
prefixSumMap.set(currentSum, (prefixSumMap.get(currentSum) || 0) + 1);
// 递归遍历左右子树
count += dfs(node.left, currentSum);
count += dfs(node.right, currentSum);
// 回溯:移除当前节点的路径和(因为要返回上一层)
prefixSumMap.set(currentSum, prefixSumMap.get(currentSum) - 1);
if (prefixSumMap.get(currentSum) === 0) {
prefixSumMap.delete(currentSum);
}
return count;
};
return dfs(root, 0);
};
4.3 方法三:迭代法(BFS + DFS)
javascript
/**
* BFS + DFS组合方法
* 时间复杂度:O(n²),空间复杂度:O(n)
*/
var pathSum = function(root, targetSum) {
if (!root) return 0;
let totalCount = 0;
const queue = [root];
// BFS遍历每个节点作为起始点
while (queue.length) {
const node = queue.shift();
// DFS搜索以当前节点为起点的所有路径
const dfs = (currentNode, currentSum) => {
if (!currentNode) return;
currentSum += currentNode.val;
if (currentSum === targetSum) {
totalCount++;
}
dfs(currentNode.left, currentSum);
dfs(currentNode.right, currentSum);
};
dfs(node, 0);
// 将子节点加入队列
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
return totalCount;
};
5. 复杂度与优缺点对比
| 方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|
| 暴力递归法 | O(n²) | O(h) | 思路直观,实现简单 | 效率低,重复计算多 | 小规模树,学习理解 |
| 前缀和优化法 | O(n) | O(n) | 效率最高,最优解 | 需要理解前缀和概念 | 大规模树,生产环境 |
| BFS+DFS组合 | O(n²) | O(n) | 结合两种遍历方式 | 效率不高,空间占用大 | 特定场景,需要层次信息 |
6. 总结与前端应用场景
6.1 核心要点总结
- 前缀和技巧是解决此类路径和问题的关键,将树路径问题转化为数组问题
- 回溯思想在树遍历中至关重要,确保状态正确恢复
- 哈希表优化是提升算法效率的常用手段
6.2 前端实际应用场景
-
DOM事件委托优化:
javascript// 类似思想:事件委托中使用Map存储事件处理器 class EventDelegator { constructor() { this.handlers = new Map(); } addHandler(path, handler) { // 存储路径与处理器的映射 this.handlers.set(path, handler); } } -
路由权限验证:
javascript// 检查用户是否有访问某个嵌套路由的权限 function checkRoutePermission(routes, requiredPermissions) { const permissionMap = new Map(); // 使用类似前缀和的思想累积权限 } -
组件状态管理:
javascript// 在React/Vue组件树中统计特定状态 function countStatePaths(componentTree, targetState) { // 使用前缀和思想查找状态组合 } -
性能监控路径分析:
javascript// 分析页面加载过程中资源加载路径 class PerformanceAnalyzer { analyzeCriticalPath(resources, targetLoadTime) { // 类似路径和问题的变体 } }