1. 题目解析与解题思路
LeetCode 112题"路径总和"是一道经典的二叉树递归问题。题目要求我们判断在给定的二叉树中,是否存在一条从根节点到叶子节点的路径,使得路径上所有节点值之和等于给定的目标值。
这个问题看似简单,但很好地考察了递归思维和二叉树遍历的基本功。我们先来看题目描述:
给定一个二叉树的根节点root和一个表示目标和的整数targetSum,判断该树中是否存在从根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和targetSum。
1.1 问题理解与示例分析
为了更好地理解题目,让我们看一个具体例子:
code复制 5
/ \
4 8
/ / \
11 13 4
/ \ \
7 2 1
假设目标值targetSum=22,那么存在路径5→4→11→2,其和为5+4+11+2=22,因此返回true。
1.2 递归解题思路
解决这类二叉树路径问题,递归是最自然的方法。我们可以这样思考:
- 从根节点开始,每次访问一个节点时,用目标值减去当前节点的值
- 如果当前节点是叶子节点(左右子节点都为空),检查剩余目标值是否为0
- 如果不是叶子节点,则递归检查左右子树
- 只要左右子树中有一个满足条件,就返回true
这种思路符合"分而治之"的算法思想,将大问题分解为小问题,直到达到基本情况(叶子节点或空节点)。
2. 代码实现与详细解析
2.1 基础代码结构
首先,我们需要了解题目提供的二叉树节点定义:
cpp复制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) {}
};
2.2 完整解决方案代码
基于上述思路,我们可以写出如下递归解法:
cpp复制class Solution {
public:
bool hasPathSum(TreeNode* root, int targetSum) {
if(root == nullptr) return false; // 空节点直接返回false
targetSum -= root->val; // 更新剩余目标值
// 如果是叶子节点,检查剩余目标值是否为0
if(root->left == nullptr && root->right == nullptr) {
return targetSum == 0;
}
// 递归检查左右子树
return hasPathSum(root->left, targetSum) || hasPathSum(root->right, targetSum);
}
};
2.3 代码逐行解析
-
空节点处理:
if(root == nullptr) return false;- 递归过程中可能遇到空节点,直接返回false
- 这也是递归的终止条件之一
-
目标值更新:
targetSum -= root->val;- 每访问一个节点,就从目标值中减去当前节点的值
- 这样后续只需要检查剩余目标值是否为0即可
-
叶子节点检查:
cpp复制if(root->left == nullptr && root->right == nullptr) { return targetSum == 0; }- 当到达叶子节点时,检查剩余目标值是否为0
- 如果是0,说明从根到该叶子的路径和等于原始目标值
-
递归调用:
cpp复制return hasPathSum(root->left, targetSum) || hasPathSum(root->right, targetSum);- 如果不是叶子节点,继续递归检查左右子树
- 使用逻辑或操作,只要有一个子树满足条件就返回true
3. 递归三要素详解
解决递归问题,必须明确三个关键要素:
3.1 递归的定义(函数的功能)
hasPathSum(root, targetSum)函数表示:以root为根的二叉树中,是否存在一条从root到叶子节点的路径,其节点值之和等于targetSum。
3.2 递归的终止条件
root == nullptr:遇到空节点,说明这条路径无效,返回falseroot是叶子节点:检查剩余目标值是否为0
3.3 递归的拆解(问题规模的缩小)
对于非叶子节点,将问题分解为两个子问题:
- 左子树是否存在路径和为
targetSum - root.val - 右子树是否存在路径和为
targetSum - root.val
只要其中一个子问题为真,整个问题就为真。
4. 复杂度分析与优化思考
4.1 时间复杂度分析
- 最坏情况下需要访问所有节点,时间复杂度为O(N),其中N是节点数量
- 对于平衡二叉树,时间复杂度为O(logN)
4.2 空间复杂度分析
- 递归调用栈的深度取决于树的高度
- 最坏情况下(树退化为链表),空间复杂度为O(N)
- 平衡二叉树情况下为O(logN)
4.3 可能的优化方向
虽然递归解法简洁明了,但在实际面试中,面试官可能会要求非递归解法。我们可以考虑使用迭代法,借助栈来实现深度优先搜索:
cpp复制bool hasPathSum(TreeNode* root, int targetSum) {
if(!root) return false;
stack<pair<TreeNode*, int>> s;
s.push({root, targetSum - root->val});
while(!s.empty()) {
auto [node, currSum] = s.top();
s.pop();
if(!node->left && !node->right && currSum == 0) {
return true;
}
if(node->right) {
s.push({node->right, currSum - node->right->val});
}
if(node->left) {
s.push({node->left, currSum - node->left->val});
}
}
return false;
}
这种迭代方法避免了递归带来的栈溢出风险,更适合处理深度很大的树。
5. 常见错误与调试技巧
5.1 常见错误类型
- 忽略空节点处理:忘记处理root为nullptr的情况
- 错误判断叶子节点:只检查了左子节点或右子节点为空
- 目标值更新错误:在递归调用前没有正确更新targetSum
- 逻辑运算符错误:使用了逻辑与(&&)而不是逻辑或(||)
5.2 调试技巧
-
打印递归路径:在递归函数中添加打印语句,跟踪当前节点和目标值
cpp复制cout << "当前节点值:" << root->val << ",剩余目标值:" << targetSum << endl; -
可视化递归过程:画出一棵简单的二叉树,手动模拟递归过程
-
边界条件测试:
- 空树
- 只有一个节点的树
- 目标值正好等于根节点值
- 目标值小于或大于所有路径和
5.3 测试用例设计
好的测试用例应该覆盖各种边界情况:
cpp复制// 测试用例1:空树
assert(hasPathSum(nullptr, 0) == false);
// 测试用例2:单节点树,匹配目标值
TreeNode* root1 = new TreeNode(5);
assert(hasPathSum(root1, 5) == true);
// 测试用例3:单节点树,不匹配目标值
assert(hasPathSum(root1, 6) == false);
// 测试用例4:多节点树,存在匹配路径
TreeNode* root2 = new TreeNode(1, new TreeNode(2), new TreeNode(3));
assert(hasPathSum(root2, 3) == true); // 1→2
// 测试用例5:多节点树,不存在匹配路径
assert(hasPathSum(root2, 5) == false);
6. 二叉树递归问题通用解法
6.1 二叉树递归模板
通过这道题,我们可以总结出解决二叉树递归问题的通用模板:
- 定义递归函数:明确函数的功能和参数含义
- 确定终止条件:通常是空节点或叶子节点
- 处理当前层逻辑:根据问题需求处理当前节点
- 递归调用:处理左子树和右子树
- 合并结果:根据需要合并左右子树的结果
6.2 类似题目推荐
掌握了这个模板后,可以尝试解决以下类似题目:
- LeetCode 113 - 路径总和 II(找出所有路径)
- LeetCode 437 - 路径总和 III(路径不需要从根开始)
- LeetCode 129 - 求根到叶子节点数字之和
- LeetCode 124 - 二叉树中的最大路径和
6.3 递归思维训练建议
- 从小树开始:先用简单的2-3层二叉树手动模拟递归过程
- 画递归树:可视化递归调用过程
- 分治思想:将大问题分解为小问题,直到可以直接解决的基本情况
- 信任递归:假设递归函数已经能解决子问题,专注于当前层的逻辑
7. 实际应用与面试技巧
7.1 实际应用场景
路径总和问题在实际中有多种应用:
- 文件系统:查找满足特定条件的文件路径
- 决策树:评估不同决策路径的成本或收益
- 游戏AI:评估可能的移动路径的价值
7.2 面试回答技巧
在面试中遇到这类问题时:
- 先确认理解:用自己的话复述问题,确保理解正确
- 举例说明:用一个简单例子演示解题思路
- 讨论边界条件:主动提出可能的特殊情况
- 代码实现:写出清晰、有注释的代码
- 测试验证:用设计的测试用例验证代码
7.3 代码风格建议
- 有意义的变量名:避免使用过于简单的变量名
- 适当注释:解释关键步骤的逻辑
- 函数拆分:如果逻辑复杂,可以拆分成辅助函数
- 错误处理:考虑可能的异常情况
8. 总结与进阶思考
这道路径总和问题很好地展示了递归在二叉树问题中的应用。关键在于:
- 明确定义递归函数的功能
- 正确处理终止条件
- 准确分解子问题
对于想进一步挑战的读者,可以思考:
- 如何优化空间复杂度?
- 如何处理非常大的树(防止栈溢出)?
- 如果要求输出所有满足条件的路径,该如何修改算法?
在实际编程中,递归虽然简洁,但需要注意栈深度问题。对于生产环境,可能需要考虑迭代解法或尾递归优化。