如果大家看过数据结构或者算法之类的资料,一定会发现,基本上大多数算法都采用了递归的思想来解决问题。虽然递归算法在运行速度和运行时所需内存上不如非递归算法,但递归算法的可读性和可维护性以及其精简的代码量使得很多人更倾向于使用递归算法来解决复杂的问题。
我来拿下面这个代码来分析递归的思想和递归的实现方法,这段代码主要判断一棵树是否存在这样的性质:从根结点到叶子结点路径上的节点的和为给定的sum。这也是leetcode上的一道题目。
public boolean hasPathSum(TreeNode root, int sum) {
if(root==null) return false;
if(root.left==null&&root.right==null&&sum==root.val) return true;
return(hasPathSum(root.left,sum-root.val) || hasPathSum(root.right,sum-root.val));
}
- 递归的能力在于用有限的语句来表示无限的集合,在这里我并不知道树有多少层深,姑且当成无限进行递归。
- 边界条件:首先确定边界条件,就是递归到最后的结果,这是递归的基础,在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口。如果没有这个出口,递归就永远的递归下去了,没有意义了。正如我们上面所说,我把它当成一个无限的循环,那么如果不用if语句强迫调用前返回,那么在调用函数后,它永远不会返回。如代码函数中1,2行。ps:在有返回值的递归函数之中,递归出口也承担着返回输出结果的作用!同样见代码函数中1,2行。
- 定义子结构:定义子结构和重复子问题是递归的精髓,同样也是分治思想和动态规划的精髓,定义子结构是说我们能够将目前所需要处理的问题分解成子问题,然后我们能根据子问题的计算结果经过一系列优化计算得出母问题的结果。如这里将当前结点的母问题转化为左右结点的子问题。
- 重复子问题:重复子问题是指子问题和母问题面对的都是同样的问题,问题中出现的不同点往往就是被子问题之间传递的参数,如这里的结点和sum。如果子问题不重复,那么定义的子结构没有任何意义,因为无法递归或采用循环来解决。
- 子问题独立:我们把这种一个母问题在对子问题选择时,当前被选择的子问题两两互不影响的情况叫做“子问题独立”。子问题独立很重要,子问题之间不能够互相干扰,这个具体的例子我们等会引用leetcode一道题目来讨论。
- 通用处理代码: 每个递归函数都要进行的运算
- 边界条件,定义子结构,重复子问题,子问题独立是设计递归算法的精髓,也是设计递归算法的步骤。并且在分治和动规之中应用广泛。
我还想说一下递归算法的执行顺序,很多同学很搞不懂递归中的代码如何执行 ,我对递归函数的执行顺序总结以下几点:
- 递归函数中,位于递归调用前的语句和各级被调函数具有相同的顺序.
- 递归函数中,位于递归调用语句后的语句的执行顺序和各个被调用函数的顺序相反.
- 即位于递归函数入口前的语句,右外往里执行;位于递归函数入口后面的语句,由里往外执行。
- 每一次函数调用总都会有一次返回.当程序流执行到某一级递归的结尾处时,它会转移到前一级递归继续执行.
- 每一级的函数调用都有自己的局部变量.非基本类型对象一定要小心,这将会牵扯到子问题独立的问题。如果你希望各个子问题有自己独立的对象引用,那必须每次调用复制原有对象而非引用或者在每次该子问题调用结束时将对象恢复到母问题调用子问题前的样子以便另一子问题不受干扰的独立的使用。
关于第五点,大家可能不清楚,没关系,我将在下面用一道题目来说明它!
阅读递归函数最容易的方法不是纠缠于它的执行过程,而是相信递归函数会顺利完成它的任务。如果你的每个步骤正确无误,你的限制条件设置正确,并且每次调用之后更接近限制条件,递归函数总是能正确的完成任务。
下面是我的一个心得,到目前为止感觉还是挺有用得,写出来供大家参考:
在编写递归算法中,递归函数尽量不要有返回值或者有基本类型的返回值(直接尾递归),如果要求必须有复杂类型的返回值,那么分成两个函数写,递归函数不要有返回值,只是在函数中改变复杂类型的值;而在驱动函数中定义复杂类型,调用递归函数,返回复杂类型
理论讲的差不多了,该讲题目了,我们从易到难,先来看一下leetcode257题:Binary Tree Path。题目要求:给定一个二叉树,以List<String>返回所有的root-to-leaf路径。
二话不说,按照我们的套路来解决问题:
1. 最优子结构: 想找到root-to-leaf路径,我可以分解为root.left-to-leaf和root.right-to-leaf。然后再前面加上”root.val->”即可。
2. 重复子问题: root.left-to-leaf,root.right-to-leaf也可以通过和root相同的方法来找。
3. 边界条件: 每到leaf结点时,将其路径add到list中,并返回不再往下走了。遇到空节点也立马返回。
4. 子问题独立: list为复杂对象,传递时传递引用拷贝,这将使各子问题公用一个list,子问题不独立!采用驱动函数法。而String并不会像复杂对象的参数传递一样传递引用,因为String是常量,如果变化会产生新的引用,不是原来的复制。
即子问题拿到引用拷贝对String的处理不会对别的子问题的String产生影响!但是list会!所以可以当作参数传入,不必担心子问题独立问题。
5. 通用处理代码(有点牵强,可有可无): path+node.val+”->”
本题目还用到深度优先搜索的思想,大家不必感觉dfs很难理解,我们常使用的三种树的遍历方法(尤其是后序遍历)正是基于dfs,即后序的第一个输出结点是树中最深处的节点!
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Solution {
List<String> list = new ArrayList<String>();
public List<String> binaryTreePaths(TreeNode root) {
dfs("",root);
return list;
}
public void dfs(String path,TreeNode node){
if(node==null) return;
if(node.left==null&&node.right==null){
path=path+node.val;
list.add(path);
return;
}
dfs(path+node.val+"->",node.left);
dfs(path+node.val+"->",node.right);
//这之后第一次已经到达了最深处结点
}
}
再来看一道有一些难度,但却很有启发性的题目,leetcode113题,PathSumⅡ。题目要求:给定一棵二叉树,以List<List<Integer>>返回所有的节点值的和为sum的root-to-leaf路径。注意这里可不是字符串了,子问题独立性的问题就出现了。
还是按照我们的套路来分析问题,前三步都很简单和上面的题差不多:
1. 定义子结构:上题一样,我们要想知道root节点有几个root-to-leaf满足和为sum,只需知道root.left和root.right有几条路径和为sum-root.val,然后将它们与root.val一起存入到一个容器(容器全是复杂对象)中。
2. 重复子问题:自然是重复的
3. 边界条件:遇到null时自然返回,遇到叶子结点判断其和是否满足要求,若满足则将list再添加到list中,否则不做任何操作,然后都return;
4. 子问题重叠:这个地方要牵扯到子问题重叠的问题,哪部分重叠了呢?list重叠了!我们用list来记录root-to-leaf,就像上一题用String一样,但list和String不一样:母问题把一个String赋值给两个子问题,其中一个子问题对String的操作不会影响到另一个子问题中的String(具体参考String的性质);而如果母问题把一个list赋值给两个子问题,其中一个子问题对list的操作必然会影响到另一个子问题中的list(对象传引用)。这就产生了重叠!
解决之道:还好我们是深度优先搜索的思想,所有的子问题并非并行处理,而是串行处理,所以我们可以在完成一个子问题后(走完一条root-to-leaf)返回的过程中,remove掉之前加入的元素。以使得开始处理另一个子问题的时候,list是干净的,丝毫看不出被处理过的样子。
正好递归语句之后的语句执行的顺序与递归调用的顺序相反,这正好符合我们这里的需求:把子问题新添加的删掉,就像一个栈一样(其实就是一个栈),递归本身就是后调用的先返回。
5. 通用处理代码:
sum=sum-node.val;
listin.add(node.val);
.....
listin.remove(listin.size()-1);
本题算法的java代码如下:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
//你把一个对象放到list中 , 然后在对这个对象修改,list中元素当然会变
public class Solution {
public List<List<Integer>> pathSum(TreeNode root, int sum) {
List<List<Integer>> list=new LinkedList<List<Integer>>();
List<Integer> listin=new LinkedList<Integer>();
dfs(list,root,listin,sum);
return list;
}
public void dfs( List<List<Integer>> list,TreeNode node,List<Integer> listin,int sum){
if(node==null) return;
sum=sum-node.val;
listin.add(node.val);
if(sum==0&&node.left==null&&node.right==null){
//当然要建一个新的放进去,这里面listin只有一个,如果不建个新的放进去,那么所有的操作就
//会干扰到已经放进去的listin。
list.add(new LinkedList<Integer>(listin));
listin.remove(listin.size()-1);
return;
}
dfs(list,node.left,listin,sum);
//不要认为调用了两次就要remove两次,最后的那个remove都会走到,前面的add是在入口处添加,
//最后的remove是在出口处清除!
//listin.removeLast();
dfs(list,node.right,listin,sum);
listin.remove(listin.size()-1);
//listin.removeLast();
//不管几层递归 前后添加删除若想不变 需要一致
}
}
最后在附上一个题,大家可以自己练习一下,还是leetcode的题目,不想写题目了给个链接和答案大家自己做做看看
129. Sum Root to Leaf Numbers
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Solution {
private int sum = 0;
public int sumNumbers(TreeNode root) {
sumR(root, 0);
return sum;
}
private void sumR(TreeNode node, int num) {
if (node == null) return;
num = 10*num + node.val;
if (node.left == null && node.right == null){
sum += num;
return;
}
sumR(node.left, num);
sumR(node.right,num);
}
}
这四个题目非常的相似,完美的解释了递归的所有注意事项,可能还有一些不全面的,这只是第一讲,我以后还会继续更新!
reference:
1.http://blog.csdn.net/speedme/article/details/21654357
2.http://www.cnblogs.com/SDJL/archive/2008/08/22/1274312.html
3.http://blog.sina.com.cn/s/blog_95c607dd010132nd.html