https://oj.leetcode.com/problems/path-sum-ii/

Given a binary tree and a sum, find all root-to-leaf paths where each path's sum equals the given sum.

For example:
Given the below binary tree and sum = 22,

              5
/ \
4 8
/ / \
11 13 4
/ \ / \
7 2 5 1

return

[
[5,4,11,2],
[5,8,4,5]
]

解题思路:

这题和上题 Path Sum 很类似,在他的基础上,要求输出所有和为sum的path。上题讲过,这种题目一般用DFS来做,那么在递归DFS的时候,同时维护一个总的结果的List<List<Integer>> resultList,和一个当前可能的子结果List<Integer> currentList。

这道题本该一次性就写出来,无奈犯了一个非常低级的语言层面的错误。符合条件的时候,直接将currentList加入到resultList中了。这样resultList中等于是存了N个一模一样的list对象了。应该每次拷贝currenList生成一个新的对象放入resultList,就不会有问题了。代码如下:

/**
* Definition for binary tree
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Solution {
public List<List<Integer>> pathSum(TreeNode root, int sum) {
List<List<Integer>> resultList = new ArrayList<List<Integer>>();
List<Integer> currentList = new ArrayList<Integer>();
DFS(root, sum, 0, resultList, currentList);
return resultList;
} public void DFS(TreeNode root, int sum, int sumCurrent, List<List<Integer>> resultList, List<Integer> currentList){
if(root == null){
return;
} currentList.add(root.val);
sumCurrent = sumCurrent + root.val;
if(root.left == null && root.right == null){
if(sumCurrent == sum){
//之前一直错,犯了低级错误!!!resultList.add(currentList);
resultList.add(new LinkedList(currentList));
}
}
DFS(root.left, sum, sumCurrent, resultList, currentList);
DFS(root.right, sum, sumCurrent, resultList, currentList);
currentList.remove(currentList.size() - 1);
//下面一行可要可不要
// sumCurrent = sumCurrent - root.val;
}
}

可是至少有一个问题,为什么同样是子状态,前面currentList就一定要回溯,currentSum就不要回溯?

上面的算法中,我们看到一个DFS+回溯算法的基本模样,前面提到的另外两道题DFS题目,Letter Combinations of a Phone Number 和 Word Break II 也都是这个模型。可以说,这种算法无论是在面试中还是实际编程中都是相当重要的,理解了它会有种豁然开朗的感觉。我们可以稍微总结一下。

首先,判断递归返回的条件,写在最开始。然后,处理当前节点。处理完当前节点后,DFS下一节点,最后回溯。或者循环处理当前节点内的所有可能的状态(比如上面电话号码簿里一个数字所有代表的字母,这时一般用for循环)。

回溯是这个算法的关键,下面我借用一个网友的伪代码,讲讲自己对上面的问题,也就是回溯,为什么要回溯的理解。引用的网址在下面有。

void dfs(int 当前状态)
{
if(当前状态为边界状态)
{
记录或输出
return;
}
for(i=0;i<n;i++) //横向遍历解答树所有子节点
{
//扩展出一个子状态。
修改了全局变量
if(子状态满足约束条件)
{
dfs(子状态)
}
恢复全局变量//回溯部分
}
}

从上面的伪代码里我们可以看到,对当前节点的一个状态的操作,往往会包含对全局变量的修改,类似于本题中的currentList。想想递归的过程,实际上是一个树形的占空间,每个子状态都有自己的栈,存放当前的临时变量,这也就是为什么有时候递归非常耗费空间的原因。

这时我们再来看上面提出的问题,为什么同样是子状态,前面currentList就一定要回溯,currentSum就不要回溯?这里我只说Java,因为自己对C或者C++不了解。

在Java里,所有方法(或函数)的参数,只有pass by value,这和C++可以传引用是非常不同的。所以所有的primitive type,比如int, char等,传的都是值。在本题中,DFS方法的参数,sum和sumCurrent传的都是他们的数值,而不是本身,他们都是每个递归状态都会维护在自己占空间内的,所以后面的修改不会影响前面栈内的值。这就是currentSum不需要回溯的原因。用上面的伪代码来说,它不是全局变量。最后一行sumCurrent = sumCurrent - root.val;是多余的,因为它减也是减去当前栈内的值,和其他栈并不相关。

反过来,currenList是一个对象。严格来说,Java中,currentList中存的是一个内存地址,这片内存内才住着真正的对象。所以DFS方法传参的时候,pass by value,就将这个地址传了进去。在整个递归的过程中,所有对currenList的修改,都是修改的这一个内存地址,也就是这一个对象。各自自状态的栈空间里,保存的副本也仅仅是这个内存地址的值。各个递归状态对currenList的修改,都会影响其他状态。这也就是为什么currentList需要回溯的原因。dfs修改后,需要立刻恢复它到修改前的状态。

有其他网友总结的更好,我来转载下。

http://blog.csdn.net/fightforyourdream/article/details/12866861

http://blog.chinaunix.net/uid-26602509-id-3178938.html

http://blog.sina.com.cn/s/blog_779fba530100wqz9.html

http://blog.csdn.net/cdfr2321388/article/details/5725863

从这个问题上,我们还能比较好的理解Java里pass by value的实质,为什么primitive就是pass by value,而object type却感觉是pass by reference,实际上也是pass by value。

那么思路来了,我们能不能将这个currenList在每次递归的栈空间里都维护一个副本,这样就不需要回溯了嘛。下面的代码便是这样的一个实现。

/**
* Definition for binary tree
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Solution {
public List<List<Integer>> pathSum(TreeNode root, int sum) {
List<List<Integer>> resultList = new ArrayList<List<Integer>>();
List<Integer> currentList = new ArrayList<Integer>();
DFS(root, sum, 0, resultList, currentList);
return resultList;
} public void DFS(TreeNode root, int sum, int sumCurrent, List<List<Integer>> resultList, List<Integer> currentList){
if(root == null){
return;
} //回溯的关键,每次申明一个新的内存放当前状态的结果currentList,后面就不要回溯了
List<Integer> soFarList = new LinkedList(currentList);
soFarList.add(root.val);
sumCurrent = sumCurrent + root.val;
if(root.left == null && root.right == null){
if(sumCurrent == sum){
//之前一直错,犯了低级错误!!!resultList.add(currentList);
resultList.add(new LinkedList(soFarList));
}
}
DFS(root.left, sum, sumCurrent, resultList, soFarList);
DFS(root.right, sum, sumCurrent, resultList, soFarList);
//下面一步就不必要了,这个对理解回溯很重要
// currentList.remove(currentList.size() - 1);
//下面一行可要可不要
// sumCurrent = sumCurrent - root.val;
}
}

应该说,回溯放在递归方法的最后面,理解起来是有点难度的,这样一解释应该能清楚很多。

这类题目在面试中经常出现,以及DFS和BFS的区别,啥时候用啥,各自优缺点之类,需要好好理解。

Path Sum II 总结DFS的更多相关文章

  1. 113. Path Sum II (Tree; DFS)

    Given a binary tree and a sum, find all root-to-leaf paths where each path's sum equals the given su ...

  2. LeetCode Path Sum II (DFS)

    题意: 给一棵二叉树,每个叶子到根的路径之和为sum的,将所有可能的路径装进vector返回. 思路: 节点的值可能为负的.这样子就必须到了叶节点才能判断,而不能中途进行剪枝. /** * Defin ...

  3. Path Sum II

    Path Sum II Given a binary tree and a sum, find all root-to-leaf paths where each path's sum equals ...

  4. 【leetcode】Path Sum II

    Path Sum II Given a binary tree and a sum, find all root-to-leaf paths where each path's sum equals ...

  5. LeetCode: Path Sum II 解题报告

    Path Sum II Given a binary tree and a sum, find all root-to-leaf paths where each path's sum equals ...

  6. [LeetCode] 113. Path Sum II ☆☆☆(二叉树所有路径和等于给定的数)

    LeetCode 二叉树路径问题 Path SUM(①②③)总结 Path Sum II leetcode java 描述 Given a binary tree and a sum, find al ...

  7. Path Sum II - LeetCode

    目录 题目链接 注意点 解法 小结 题目链接 Path Sum II - LeetCode 注意点 不要访问空结点 解法 解法一:递归,DFS.每当DFS搜索到新节点时,都要保存该节点.而且每当找出一 ...

  8. Leetcode: mimimum depth of tree, path sum, path sum II

    思路: 简单搜索 总结: dfs 框架 1. 需要打印路径. 在 dfs 函数中假如 vector 变量, 不用 & 修饰的话就不需要 undo 2. 不需要打印路径, 可设置全局变量 ans ...

  9. [Leetcode Week14]Path Sum II

    Path Sum II 题解 原创文章,拒绝转载 题目来源:https://leetcode.com/problems/path-sum-ii/description/ Description Giv ...

随机推荐

  1. JavaScript中比较运算符的使用

    比较运算符的基本操作过程是:首先对操作数进行比较,这个操作数可以是数字也可以是字符串,然后返回一个布尔值true或false. 在JavaScript中常用的比较运算符如下表所示. 例如,某商场店庆搞 ...

  2. Project Management -- How to use GitHub with Git

    如何通过Git使用GitHub ——PM曾子轩 从未使用过Git 一.从官网下载Git 此部分略 二.用Git连接上GitHub(为保持完整性,此部分引用博客:https://www.cnblogs. ...

  3. 看懂SqlServer执行计划

    在园子看到一篇SQLServer关于查询计划的好文,激动啊,特转载.原文出自:http://www.cnblogs.com/fish-li/archive/2011/06/06/2073626.htm ...

  4. 阶乘问题-----sum随变量改变而改变

  5. MyEclipse加入jquery.js文件missing semicolon的错误

    今天打开项目,发现有一个小红叉,虽然不影响项目的编译和运行,但是看着非常影响心情.原因是jquery-1.8.2.min.js报了一堆missing semicolon的错误.之所以会这样,其实是My ...

  6. 图像局部显著性—点特征(Fast)

    fast作为几乎最快的角点检测算法,一般说明不附带描述子.参考综述:图像的显著性检测--点特征 详细内容,请拜访原=文章:Fast特征点检测算法 在局部特征点检测快速发展的时候,人们对于特征的认识也越 ...

  7. 【Web缓存机制系列】2 – Web浏览器的缓存机制-(新鲜度 校验值)

    Web缓存的工作原理 所有的缓存都是基于一套规则来帮助他们决定什么时候使用缓存中的副本提供服务(假设有副本可用的情况下,未被销毁回收或者未被删除修改).这些规则有的在协议中有定义(如HTTP协议1.0 ...

  8. PyCharm 恢复默认设置 | JetBrains IDE 配置文件安装目录

    网上的答案为什么都乱七八糟并且全都全篇一律?某度知道是发源地? 先说 Mac 按需运行下面的 rm 删除命令 # Configuration rm -rf ~/Library/Preferences/ ...

  9. 关于Arrays协助类中的排序方法

    sort方法是优化的快速排序,不稳定. paralleSort是多线程排序,稳定,但是有长度限制.

  10. socket主要函数介绍

    1.   基本套接字函数(1)socket函数原型 socket(建立一个socket文件描述符) 所需头文件 #include <sys/types.h> #include <sy ...