LeetCode 第113题:路径总和 II
题目描述
给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。
难度
中等
题目链接
示例
示例 1:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]
示例 2:
输入:root = [1,2,3], targetSum = 5
输出:[]
示例 3:
输入:root = [1,2], targetSum = 0
输出:[]
提示
- 树中节点总数在范围
[0, 5000]内 -1000 <= Node.val <= 1000-1000 <= targetSum <= 1000
解题思路
方法一:深度优先搜索(DFS)+ 回溯
这道题是第112题的扩展,不仅需要判断是否存在满足条件的路径,还需要找出所有满足条件的路径。我们可以使用深度优先搜索(DFS)结合回溯的方法来解决。
关键点:
- 使用一个路径列表记录当前遍历的路径
- 使用回溯法遍历所有可能的路径
- 当到达叶子节点且路径和等于目标和时,将当前路径添加到结果中
- 注意在回溯时需要移除当前节点
具体步骤:
- 定义一个结果列表和一个当前路径列表
- 从根节点开始递归遍历
- 将当前节点添加到路径中
- 如果当前节点是叶子节点且路径和等于目标和,将当前路径添加到结果中
- 递归遍历左右子树
- 回溯时从路径中移除当前节点
- 返回所有满足条件的路径
时间复杂度:O(n²),其中n是树中节点的数量,最坏情况下需要遍历所有节点,且每找到一条路径都需要O(n)的时间复制路径 空间复杂度:O(n),递归调用栈的深度和路径列表的空间
方法二:广度优先搜索(BFS)
我们也可以使用广度优先搜索(BFS)来解决这个问题,但需要额外记录每个节点的父节点和路径和。
关键点:
- 使用队列进行层序遍历
- 记录每个节点的父节点,以便回溯构建路径
- 当找到满足条件的叶子节点时,通过父节点指针回溯构建路径
具体步骤:
- 如果根节点为空,返回空列表
- 初始化队列,将根节点入队
- 使用哈希表记录每个节点的父节点和从根节点到当前节点的路径和
- 当队列不为空时,执行以下操作: a. 出队一个节点 b. 如果当前节点是叶子节点且路径和等于目标和,通过父节点指针回溯构建路径并添加到结果中 c. 将当前节点的左右子节点入队,并更新它们的父节点和路径和
- 返回所有满足条件的路径
时间复杂度:O(n²),最坏情况下需要遍历所有节点,且每找到一条路径都需要O(n)的时间回溯构建路径 空间复杂度:O(n),队列和哈希表的空间
图解思路
方法一:DFS + 回溯过程分析表
以示例1为例,targetSum = 22
| 当前节点 | 当前路径 | 当前和 | 是否叶子节点 | 是否满足条件 | 操作 |
|---|---|---|---|---|---|
| 5 | [5] | 5 | 否 | - | 继续递归 |
| 4 | [5,4] | 9 | 否 | - | 继续递归 |
| 11 | [5,4,11] | 20 | 否 | - | 继续递归 |
| 7 | [5,4,11,7] | 27 | 是 | 否 | 回溯 |
| 2 | [5,4,11,2] | 22 | 是 | 是 | 添加路径到结果,回溯 |
| 8 | [5,8] | 13 | 否 | - | 继续递归 |
| 13 | [5,8,13] | 26 | 是 | 否 | 回溯 |
| 4 | [5,8,4] | 17 | 否 | - | 继续递归 |
| 5 | [5,8,4,5] | 22 | 是 | 是 | 添加路径到结果,回溯 |
| 1 | [5,8,4,1] | 18 | 是 | 否 | 回溯 |
方法二:路径回溯构建过程表
| 满足条件的叶子节点 | 回溯路径构建过程 | 最终路径 |
|---|---|---|
| 2 | 2 -> 11 -> 4 -> 5 | [5,4,11,2] |
| 5 | 5 -> 4 -> 8 -> 5 | [5,8,4,5] |
代码实现
C# 实现
/**
* Definition for a binary tree node.
* public class TreeNode {
* public int val;
* public TreeNode left;
* public TreeNode right;
* public TreeNode(int val=0, TreeNode left=null, TreeNode right=null) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
public class Solution {
// 方法一:深度优先搜索(DFS)+ 回溯
public IList<IList<int>> PathSum(TreeNode root, int targetSum) {
List<IList<int>> result = new List<IList<int>>();
List<int> currentPath = new List<int>();
// 辅助函数进行DFS
DFS(root, targetSum, currentPath, result);
return result;
}
private void DFS(TreeNode node, int targetSum, List<int> currentPath, List<IList<int>> result) {
// 如果节点为空,直接返回
if (node == null) {
return;
}
// 将当前节点添加到路径中
currentPath.Add(node.val);
// 如果是叶子节点且路径和等于目标和,将当前路径添加到结果中
if (node.left == null && node.right == null && targetSum == node.val) {
// 注意需要创建一个新的列表,否则后续回溯会影响结果
result.Add(new List<int>(currentPath));
}
// 递归遍历左右子树
DFS(node.left, targetSum - node.val, currentPath, result);
DFS(node.right, targetSum - node.val, currentPath, result);
// 回溯,移除当前节点
currentPath.RemoveAt(currentPath.Count - 1);
}
// 方法二:广度优先搜索(BFS)
public IList<IList<int>> PathSumBFS(TreeNode root, int targetSum) {
List<IList<int>> result = new List<IList<int>>();
// 如果根节点为空,直接返回空列表
if (root == null) {
return result;
}
// 使用队列进行BFS
Queue<TreeNode> nodeQueue = new Queue<TreeNode>();
Queue<int> sumQueue = new Queue<int>();
Dictionary<TreeNode, TreeNode> parentMap = new Dictionary<TreeNode, TreeNode>();
nodeQueue.Enqueue(root);
sumQueue.Enqueue(root.val);
parentMap[root] = null;
while (nodeQueue.Count > 0) {
TreeNode node = nodeQueue.Dequeue();
int currentSum = sumQueue.Dequeue();
// 如果是叶子节点且路径和等于目标和
if (node.left == null && node.right == null && currentSum == targetSum) {
// 回溯构建路径
List<int> path = new List<int>();
TreeNode current = node;
while (current != null) {
path.Add(current.val);
current = parentMap[current];
}
// 路径是从叶子节点到根节点的,需要反转
path.Reverse();
result.Add(path);
}
// 将左右子节点入队
if (node.left != null) {
nodeQueue.Enqueue(node.left);
sumQueue.Enqueue(currentSum + node.left.val);
parentMap[node.left] = node;
}
if (node.right != null) {
nodeQueue.Enqueue(node.right);
sumQueue.Enqueue(currentSum + node.right.val);
parentMap[node.right] = node;
}
}
return result;
}
}
Python 实现
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
# 方法一:深度优先搜索(DFS)+ 回溯
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
result = []
current_path = []
def dfs(node, target):
# 如果节点为空,直接返回
if not node:
return
# 将当前节点添加到路径中
current_path.append(node.val)
# 如果是叶子节点且路径和等于目标和,将当前路径添加到结果中
if not node.left and not node.right and target == node.val:
# 创建一个新的列表,避免后续回溯影响结果
result.append(current_path[:])
# 递归遍历左右子树
dfs(node.left, target - node.val)
dfs(node.right, target - node.val)
# 回溯,移除当前节点
current_path.pop()
dfs(root, targetSum)
return result
# 方法二:广度优先搜索(BFS)
def pathSumBFS(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
result = []
# 如果根节点为空,直接返回空列表
if not root:
return result
# 使用队列进行BFS
node_queue = collections.deque([root])
sum_queue = collections.deque([root.val])
parent_map = {root: None}
path_map = {root: [root.val]}
while node_queue:
node = node_queue.popleft()
current_sum = sum_queue.popleft()
current_path = path_map[node]
# 如果是叶子节点且路径和等于目标和
if not node.left and not node.right and current_sum == targetSum:
result.append(current_path)
# 将左右子节点入队
if node.left:
node_queue.append(node.left)
sum_queue.append(current_sum + node.left.val)
path_map[node.left] = current_path + [node.left.val]
parent_map[node.left] = node
if node.right:
node_queue.append(node.right)
sum_queue.append(current_sum + node.right.val)
path_map[node.right] = current_path + [node.right.val]
parent_map[node.right] = node
return result
C++ 实现
/**
* 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 {
public:
// 方法一:深度优先搜索(DFS)+ 回溯
vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
vector<vector<int>> result;
vector<int> currentPath;
dfs(root, targetSum, currentPath, result);
return result;
}
private:
void dfs(TreeNode* node, int targetSum, vector<int>& currentPath, vector<vector<int>>& result) {
// 如果节点为空,直接返回
if (node == nullptr) {
return;
}
// 将当前节点添加到路径中
currentPath.push_back(node->val);
// 如果是叶子节点且路径和等于目标和,将当前路径添加到结果中
if (node->left == nullptr && node->right == nullptr && targetSum == node->val) {
result.push_back(currentPath);
}
// 递归遍历左右子树
dfs(node->left, targetSum - node->val, currentPath, result);
dfs(node->right, targetSum - node->val, currentPath, result);
// 回溯,移除当前节点
currentPath.pop_back();
}
public:
// 方法二:广度优先搜索(BFS)
vector<vector<int>> pathSumBFS(TreeNode* root, int targetSum) {
vector<vector<int>> result;
// 如果根节点为空,直接返回空列表
if (root == nullptr) {
return result;
}
// 使用队列进行BFS
queue<TreeNode*> nodeQueue;
queue<int> sumQueue;
unordered_map<TreeNode*, TreeNode*> parentMap;
nodeQueue.push(root);
sumQueue.push(root->val);
parentMap[root] = nullptr;
while (!nodeQueue.empty()) {
TreeNode* node = nodeQueue.front();
nodeQueue.pop();
int currentSum = sumQueue.front();
sumQueue.pop();
// 如果是叶子节点且路径和等于目标和
if (node->left == nullptr && node->right == nullptr && currentSum == targetSum) {
// 回溯构建路径
vector<int> path;
TreeNode* current = node;
while (current != nullptr) {
path.push_back(current->val);
current = parentMap[current];
}
// 路径是从叶子节点到根节点的,需要反转
reverse(path.begin(), path.end());
result.push_back(path);
}
// 将左右子节点入队
if (node->left != nullptr) {
nodeQueue.push(node->left);
sumQueue.push(currentSum + node->left->val);
parentMap[node->left] = node;
}
if (node->right != nullptr) {
nodeQueue.push(node->right);
sumQueue.push(currentSum + node->right->val);
parentMap[node->right] = node;
}
}
return result;
}
};
执行结果
C# 实现
- 执行用时:128 ms
- 内存消耗:42.1 MB
Python 实现
- 执行用时:44 ms
- 内存消耗:18.6 MB
C++ 实现
- 执行用时:4 ms
- 内存消耗:19.8 MB
性能对比
| 语言 | 执行用时 | 内存消耗 | 特点 |
|---|---|---|---|
| C# | 128 ms | 42.1 MB | 代码结构清晰,但性能较慢 |
| Python | 44 ms | 18.6 MB | 代码简洁,性能适中 |
| C++ | 4 ms | 19.8 MB | 执行速度最快,内存占用适中 |
代码亮点
- 🎯 DFS + 回溯方法简洁高效,直接反映了问题的递归本质
- 💡 在回溯过程中正确处理路径的添加和移除,避免了路径混淆
- 🔍 BFS方法通过记录父节点信息,实现了路径的回溯构建
- 🎨 两种方法各有优势,DFS代码简洁,BFS避免了深度过大导致的栈溢出
常见错误分析
- 🚫 忘记在添加路径到结果时创建新的列表,导致后续回溯影响已添加的结果
- 🚫 没有正确处理叶子节点的定义,错误地将非叶子节点也计入结果
- 🚫 在BFS方法中忘记反转路径,导致路径顺序错误
- 🚫 没有正确更新目标和,导致路径判断错误
解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| DFS + 回溯 | O(n²) | O(n) | 代码简洁,直观易懂 | 对于高度较大的树可能导致栈溢出 |
| BFS | O(n²) | O(n) | 避免栈溢出,适用于所有树 | 实现复杂,需要额外空间存储父节点信息 |
相关题目
- LeetCode 112. 路径总和 - 简单
- LeetCode 437. 路径总和 III - 中等
- LeetCode 257. 二叉树的所有路径 - 简单 </rewritten_file>