前言
在刷 LeetCode 二叉树相关的题目时,"路径总和"是一道非常经典的必刷题。很多初学者在第一次尝试递归时,思路是对的,但往往会因为"节点值扣除错误"而掉进死胡同(比如测试用例 [1,2] 莫名其妙输出 true)。今天这篇博客,我们就来复盘一下这个常见错误,并一次性给出 C、C++ 和 Python 的递归与非递归(迭代)全套模板,直接帮你把这道题彻底吃透!
一、 常见踩坑记录:为什么 [1,2] 会输出 true?
在实现向下递归减去节点值时,很容易写出类似下面这样的逻辑:
错误思路 :在父节点时计算
targetSum - 父节点的值,向左/右子树递归时,再次 传入targetSum - 父节点的值。
这样会导致什么问题呢?
当输入树为 root = [1, 2](根为 1,左子节点为 2),且 targetSum = 2 时:
-
根节点 1 扣除自己的值:
2 - 1 = 1。 -
进入左子树 2,却再次减去了父节点 1 的值 :
1 - 1 = 0。 -
此时到了叶子节点 2,程序一看剩余和为 0,直接返回
true。本质原因:叶子节点的值(2)根本没有参与计算,导致了严重的误判!
正确的解题核心 :"走到哪里,就算到哪里" 。要么在传参时减去子节点 的值,要么让每个节点只负责处理当前这层的值。
二、 优雅的递归法 (DFS)
最推荐的写法是不需要辅助函数,直接利用原函数。每次递归将 targetSum 减去当前节点的值,直到叶子节点判断剩余的值是否与叶子节点的值相等。
1. C / C++ 递归版本
cpp
// C 和 C++ 的代码在递归法下完全一致
bool hasPathSum(struct TreeNode* root, int targetSum) {
// 1. 如果节点为空,说明路径不通
if (!root) return false;
// 2. 如果是叶子节点,判断当前剩余的 targetSum 是否等于自己的值
if (!root->left && !root->right) {
return targetSum == root->val;
}
// 3. 不是叶子节点,向左右子树继续寻找
return hasPathSum(root->left, targetSum - root->val) ||
hasPathSum(root->right, targetSum - root->val);
}
2. Python 递归版本
python
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
if not root:
return False
if not root.left and not root.right:
return targetSum == root.val
return self.hasPathSum(root.left, targetSum - root.val) or \
self.hasPathSum(root.right, targetSum - root.val)
三、 进阶:非递归 / 迭代法
在实际工程中,如果树的深度非常大,递归可能会导致栈溢出。掌握非递归(迭代法)是非常有必要的。迭代法的核心是利用栈(Stack)来模拟递归过程。栈中需要同时保存"当前节点"和"从根到当前节点的路径和"。
1. C 语言迭代版
由于纯 C 语言没有标准库的 Stack,我们需要用数组来模拟。通常 LeetCode 的树节点最多几千个,开一个万级数组足够了。
objectivec
bool hasPathSum(struct TreeNode* root, int targetSum) {
if (!root) return false;
// 使用两个数组模拟栈:一个存节点,一个存当前路径和
struct TreeNode* nodeStack[10000];
int sumStack[10000];
int top = -1; // 栈顶指针
// 根节点入栈
nodeStack[++top] = root;
sumStack[top] = root->val;
while (top >= 0) {
// 出栈
struct TreeNode* node = nodeStack[top];
int currentSum = sumStack[top--];
// 如果是叶子节点,且路径和等于 targetSum,返回 true
if (!node->left && !node->right && currentSum == targetSum) {
return true;
}
// 右子节点入栈(注意:栈是后进先出,先压右再压左,出栈时就是先左后右)
if (node->right) {
nodeStack[++top] = node->right;
sumStack[top] = currentSum + node->right->val;
}
// 左子节点入栈
if (node->left) {
nodeStack[++top] = node->left;
sumStack[top] = currentSum + node->left->val;
}
}
return false;
}
2. C++ 迭代版
C++ 有了 STL 就方便很多了,我们可以用 std::stack 配合 std::pair 来存储节点和当前和。
cpp
#include <stack>
#include <utility>
class Solution {
public:
bool hasPathSum(TreeNode* root, int targetSum) {
if (!root) return false;
std::stack<std::pair<TreeNode*, int>> st;
st.push({root, root->val});
while (!st.empty()) {
auto [node, currentSum] = st.top();
st.pop();
// 到达叶子节点并验证总和
if (!node->left && !node->right && currentSum == targetSum) {
return true;
}
// 压入左右子节点(先右后左)
if (node->right) {
st.push({node->right, currentSum + node->right->val});
}
if (node->left) {
st.push({node->left, currentSum + node->left->val});
}
}
return false;
}
};
3. Python 迭代版
Python 中直接使用 list 就可以完美模拟栈的操作(append 压栈,pop 出栈)。
python
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
if not root:
return False
# 栈里存储元组:(节点, 到达该节点时的路径和)
stack = [(root, root.val)]
while stack:
node, current_sum = stack.pop()
if not node.left and not node.right and current_sum == targetSum:
return True
if node.right:
stack.append((node.right, current_sum + node.right.val))
if node.left:
stack.append((node.left, current_sum + node.left.val))
return False
四、 复杂度分析
无论是递归还是迭代,本质都是深度优先搜索 (DFS):
-
时间复杂度:O(N),其中 N 是二叉树的节点数。每个节点最多被访问一次。
-
空间复杂度:O(H),其中 H 是二叉树的高度。在最坏情况下(树退化为链表),空间复杂度为 O(N);在最好情况(平衡二叉树)下,空间复杂度为 O(log N)。
总结:解决树的问题时,边界条件(是否为空)和递归终止条件(是否为叶子节点)是最关键的。理清这两点,代码自然水到渠成!
最后照例贴上卡哥的代码随想录