113. 路径总和 II
「题目:」
给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。
「示例:」
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22,输出:[[5,4,11,2],[5,8,4,5]]。
「解题思路:」
利用递归思想可以枚举出根节点到叶子节点的所有路径,但是在这个过程中需要记录当前节点的和值以及整条路径,才能获取最终的结果。
针对和值的问题,如果通过和值来对比的话,则需要引入临时变量来保存当前路径的总和,这里一般通过减法来消元。
而对于路径的记录,可以采用两种方式:
通过特殊字符分割元素的字符串来记录,但是在路径满足条件时,需要将其转化为数组。
通过数组来保存,但是要考虑引用对象在递归过程不断修改的影响。(笔者这里倾向于数组的方式,哈哈哈)
最后就是保存路径的条件:当前根节点的左右子树为空并且当前和值为0。
时间复杂度:O(n),空间复杂度:O(n)。
const pathSum = (root, sum) => {
const ans = [];
dfs(root, sum, [], ans);
return ans;
}
function dfs(root, sum, path, ans) {
if (!root) {
return;
}
const _sum = sum - root.val;
if (!root.left && !root.right && _sum === 0) {
ans.push([...path, root.val])
}
dfs(root.left, _sum, [...path, root.val], ans);
dfs(root.right, _sum, [...path, root.val], ans);
}
上述解法中,保存的路径数组每次都需要传递给下层递归过程,这里可以通过递归之后向上回溯的过程回滚数组状态,从而将路径数组优化成一个全局变量。
const pathSum = (root, sum) => {
const ans = [];
const path = [];
const dfs = (root, sum) => {
if (!root) {
return;
}
path.push(root.val);
const _sum = sum - root.val;
if (!root.left && !root.right && _sum === 0) {
ans.push([...path]);
}
dfs(root.left, _sum);
dfs(root.right, _sum);
path.pop();
}
dfs(root, sum);
return ans;
}
437. 路径总和 III
「题目:」
给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
「示例:」
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8,输出:3。
「第一种解法:深度优先搜索」
利用深度优先搜索可以很容易地枚举出以当前二叉树的根节点为起始点的所有路径,然后在递归的过程中更新剩余的和值,即可找到满足条件的路径。
function rootSum (root, sum) {
if (!root) {
return 0;
}
let ans = 0;
if (root.val === sum) {
ans++;
}
ans += rootSum(root.left, sum - root.val);
ans += rootSum(root.right, sum - root.val);
return ans;
}
但是,本道题中的路径是可以从任意节点开始的,所以需要以所有的节点元素为根节点进行深度优先搜索。
时间复杂度:O(n^2),空间复杂度:O(n)。
const pathSum = (root, sum) => {
if (!root) {
return 0;
}
return rootSum(root, sum) + pathSum(root.left, sum) + pathSum(root.right, sum);
}
「优化解法:前缀和」
上述解法中由于以每一个节点为根节点进行深度优先搜索,所以会存在许多路径被重复计算。
之所以需要对每一个节点进行深度优先搜索,是因为无法在一条路径上确定其中是否包含满足目标值的路径,而解决这个问题的技巧就是前缀和。
在搜索的过程中,通过判断当前前缀和与目标值的差值是否存在于前缀和哈希表中,来确定当前遍历的路径上是否包含满足目标值的路径和。
以示例中的二叉树为例,当从根节点 10 遍历到节点 3 时,形成的前缀和记录如下:
{
10: 1, // 节点 10 的前缀和
15: 1, // 节点 5 的前缀和
18: 1, // 节点 3 的前缀和
}
节点 3 的前缀和与目标值的差值存在于前缀和记录中,那么就存在 5 -> 3 是满足条件值的路径。
当深度优先搜索退出当前节点,需要清除当前前缀和的记录。
时间复杂度:O(n),空间复杂度:O(n)。
const pathSum = (root, sum) => {
if (!root) {
return 0;
}
const prefixSumMap = new Map();
prefixSumMap.set(0, 1);
const dfs = (root, currentSum) => {
if (!root) {
return 0;
}
currentSum += root.val;
let count = getMapByDefault(prefixSumMap, currentSum - sum, 0)
prefixSumMap.set(currentSum, getMapByDefault(prefixSumMap, currentSum, 0) + 1)
count += dfs(root.left, currentSum) + dfs(root.right, currentSum);
prefixSumMap.set(currentSum, getMapByDefault(prefixSumMap, currentSum, 0) - 1)
return count;
}
return dfs(root, 0);
}
function getMapByDefault (map, key, defaultValue) {
if (!map.has(key)) {
return defaultValue;
}
return map.get(key);
}
49. 从根节点到叶节点的路径数字之和
「题目:」
给定一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字。
每条从根节点到叶节点的路径都代表一个数字:例如,从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。
计算从根节点到叶节点生成的 所有数字之和 。
「示例:」
输入:root = [1,2,3],输出:25。
「解题思路:」
本道题的核心仍然是基于深度优先搜索来遍历所有从根节点到叶子节点的路径,在遍历的过程中需要注意两点:
递归遍历的过程中利用字符串来保存访问元素的序列。
当前节点不存在左右子树的情况下,此节点为叶子节点,应该进行和值计算操作。
时间复杂度:O(n),空间复杂度:O(n)。
const sumNumbers = root => {
let ans = 0;
const dfs = (root, path) => {
if (!root) {
return '';
}
path += root.val;
if (!root.left && !root.right) {
ans += Number.parseInt(path);
}
dfs(root.left, path);
dfs(root.right, path);
}
dfs(root, '');
return ans;
}
写在最后
LeetCode 数据结构篇根据难度划分为:
数据结构入门篇
数据结构进阶篇
数据结构高级进阶篇
每篇会根据题目的类型和解题思路形成不同的专题,这样更利于摸清本质,将技巧内化于心。
感兴趣的小伙伴可以关注下方的公众号,或者微信搜索“漫谈大前端”,如果本文对你有帮助,欢迎点赞、分享、转发。
相关链接:
https://leetcode.cn/problems/path-sum-ii/
https://leetcode.cn/problems/path-sum-iii/
https://leetcode.cn/problems/3Etpl5/