路径总和 II
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
无条件进入子树,检查叶子节点,全局变量。
在前序位置添加当前节点到路径中后,要更新路径和并检查更新后的状态是否是叶子节点且路径和是否为0
回溯包括DFS。DFS是先取一个满足条件的点,然后进入递归函数,回溯是无差别,直接从头开始递归。
class Solution {
List<List<Integer>> ret = new LinkedList<List<Integer>>();
Deque<Integer> path = new LinkedList<Integer>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
dfs(root, targetSum);
return ret;
}
public void dfs(TreeNode root, int targetSum) {
// 特殊条件
if (root == null) {
return;
}
// offerLast返回boolean值,addLast出错的话会抛出异常
path.offerLast(root.val);
// 更新状态
targetSum -= root.val;
// 检查更新后的状态是否成熟。这段if代码放在前中后都可以,因为它是结束条件,与遍历顺序无关。
if (root.left == null && root.right == null && targetSum == 0) {
ret.add(new LinkedList<Integer>(path));
// 下面两行代码可加可不加。但必须同时加或不加。
// 返回到上一层递归前需要去掉root。
path.removeLast();
return;
}
// 我们要找到所有答案,且节点值可能为负,所以就是本步状态成熟也要继续遍历子树。意思就是不是叶子节点但是targetSum为0,也要继续递归。
dfs(root.left, targetSum);
dfs(root.right, targetSum);
path.pollLast();
// targetSum是局部变量,因此每次递归都有自己的独立的targetSum,不需要再执行targetSum += root.val。path是全局的,所以它要执行pollLast来复原。
}
}
时间复杂度:O(N^2),其中 N 是树的节点数。在最坏情况下,树的上半部分为链状,下半部分为完全二叉树,并且从根节点到每一个叶子节点的路径都符合题目要求。此时,路径的数目为 O(N),并且每一条路径的节点个数也为 O(N),因此要将这些路径全部添加进答案中,时间复杂度为 O(N^2)。
解释一下时间复杂度:上半部分是N/2个节点,下半部分是N/2个,完全二叉树的高度是log2N/2,所以叶子节点的个数是 2log2N/2=N/2 个,所以时间复杂度总共是 O(N^2)。
空间复杂度:O(N),其中 N 是树的节点数。空间复杂度主要取决于栈空间的开销,栈中的元素个数不会超过树的节点数。
BFS
一个队列存节点,一个队列存路径和,一个临时变量存路径和,一个哈希表存父节点以获取路径。
因为我们需要获取所有的满足条件的路径,所以需要一个队列来存路径和。
class Solution {
// 用于存储所有符合条件的路径
List<List<Integer>> ret = new LinkedList<List<Integer>>();
// 哈希表用于记录每个节点的父节点,以便于回溯路径
Map<TreeNode, TreeNode> map = new HashMap<TreeNode, TreeNode>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
// 如果根节点为空,直接返回空列表
if (root == null) {
return ret;
}
// 使用两个队列,分别存储节点和从根到当前节点的路径和
Queue<TreeNode> queueNode = new LinkedList<TreeNode>();
Queue<Integer> queueSum = new LinkedList<Integer>();
// 初始化队列
queueNode.offer(root);
queueSum.offer(root.val);
// 广度优先搜索
while (!queueNode.isEmpty()) {
TreeNode node = queueNode.poll();
int rec = queueSum.poll(); // 更新当前路径和
// 如果当前节点是叶子节点
if (node.left == null && node.right == null) {
// 如果当前路径和等于目标值,回溯并记录这条路径
if (rec == targetSum) {
getPath(node);
}
} else {
// 如果不是叶子节点,继续探索其子节点
if (node.left != null) {
map.put(node.left, node); // 记录左子节点的父节点
queueNode.offer(node.left); // 左子节点入队
queueSum.offer(rec + node.left.val); // 更新路径和入队
}
if (node.right != null) {
map.put(node.right, node); // 记录右子节点的父节点
queueNode.offer(node.right); // 右子节点入队
queueSum.offer(rec.right.val); // 更新路径和入队
}
}
}
return ret;
}
// 从叶子节点回溯到根节点,记录路径
public void getPath(TreeNode node) {
List<Integer> temp = new LinkedList<Integer>();
// 从当前节点开始,通过哈希表回溯到根节点
while (node != null) {
temp.add(0, node.val); // 将节点值添加到列表的开头,避免后续的翻转操作
node = map.get(node); // 移动到父节点
}
// 一定要新建列表,不能直接add(temp),因为temp是引用类型,会被修改。
ret.add(new LinkedList<Integer>(temp));
}
}
- 时间复杂度:O(N^2),其中 N 是树的节点数。分析思路与方法一相同。
- 空间复杂度:O(N),其中 N 是树的节点数。空间复杂度主要取决于哈希表和队列空间的开销,哈希表需要存储除根节点外的每个节点的父节点,队列中的元素个数不会超过树的节点数。
有返回值有信息参数的DFS,后序归并。
class Solution {
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
List<List<Integer>> rootAnswers = new LinkedList<>();
// 特殊条件
if (root == null) {
return rootAnswers;
}
// 结束条件:如果是叶子节点并且值等于 targetSum,则找到一条路径。这段代码在后序也可以。
if (root.left == null && root.right == null && root.val == targetSum) {
// 成功时才新建一个path
LinkedList<Integer> path = new LinkedList<>();
// path不是全局变量,所以不需要回溯,即pollLast()。
path.add(root.val);
rootAnswers.add(path);
return rootAnswers;
}
// 分别递归左右子树,找到子树中以子节点为起点以叶子节点为终点的和为 targetSum - root.val 的路径
List<List<Integer>> leftAnswers = pathSum(root.left, targetSum - root.val);
List<List<Integer>> rightAnswers = pathSum(root.right, targetSum - root.val);
// 左右子树的路径加上根节点,就是和为 targetSum 的路径
for (List<Integer> answer : leftAnswers) {
// 在链表头部添加root.val
// 因为底层使用的是 LinkedList,所以这个操作的复杂度是 O(1)
answer.add(0, root.val);
rootAnswers.add(answer);
}
for (List<Integer> answer : rightAnswers) {
// 在链表头部添加root.val
// 因为底层使用的是 LinkedList,所以这个操作的复杂度是 O(1)
answer.add(0, root.val);
rootAnswers.add(answer);
}
return rootAnswers;
}
}