大师 L. Peter Deutsch 说过:To Iterate is Human, to Recurse, Divine.中文译为:人理解迭代,神理解递归。
通俗的讲:函数自己调自己;
再举一例子:比如说在电影院,你想知道自己在第几排,但是人太多黑咕隆咚的你也不能去数,怎么办呢,你就问前面那个人你在第几排,前面的和你一样也问前面的,这样到第一排了,他知道自己在第一排,然后又回传给第二个人,一直回传最后你就知道自己在第几排了。
递归的内涵:递归是一个有去有回的过程。
想办法写出一个递归程序需要明确这里面的三要素;
对于递归,首先不用管函数里面的代码应该是什么样的,而是我们首先明白这个函数是做什么的,能完成什么功能。
比如说我们要让求一个n的前n项和。我们首先写出了这个函数:
public int add(int n){ //那到现在我们先不用管这里面怎么写了 //我们首先明白我们这个函数调用它,给它传个n进去就能得到1到n的和。 }
函数自己调自己总不能一直调下去吧?想一下上面的例子,如果电影院根本没有头,那你一直在等着,答案也不可能回传回来,所以一定要有一个终止条件,然后我们就能把结果返回回来。
这个终止条件的意思是:我们必须根据这个参数值,能直接得到我的功能结果。
比如我们想知道自己在第几排,我们能够根据前面没有人了,直接得到我就在第一排;比如我们想求前n项和,我们能够根据n为1的时候,直接得到和就为1;
所以我们可以把上面代码补上第二要素;
public int add(int n){ //第二要素:我们能够根据n为1直接得到前n项和为1. if(n == 1) return 1; }
第三个要素就是我们要找出函数的等价关系,不断的缩小问题规模
其实在寻找等价关系,求解递归问题的时候右两种模型,想一下我们这个递归的过程,有去有回,所以可以在递去的过程中解决问题,亦可以在归来的时候解决问题,这就产生了两种模型。
模型一:在递去的过程中解决问题
这类基本上适用于没有明确的等价关系,而更多的时候是一种过去的时候就可以输出,比如打印,遍历之类的,或者说需要有判断条件的时候,在过去的时候进行判断,满足就可以左一些处理等。这类问题上,比如说二叉树的遍历,二叉树的左叶子节点之和等。
function recursion(大规模){ if (end_condition){ // 明确的递归终止条件 end; // 简单情景 }else{ // 在将问题转换为子问题的每一步,解决该步中剩余部分的问题 solve; // 递去 recursion(小规模); // 递到最深处后,不断地归来 } }
模型二:在归来的过程中解决问题
这类常用于能够明确的看出来基本的等价关系上,能够根据获得的小范围的结果推倒出大范围结果的时候,比如说前n项和,斐波那契数列等。都能够根据n-1的结果得到n的结果,这就是在归来的时候解决问题。
function recursion(大规模){ if (end_condition){ // 明确的递归终止条件 end; // 简单情景 }else{ // 先将问题全部描述展开,再由尽头“返回”依次解决每步中剩余部分的问题 recursion(小规模); // 递去 solve; // 归来 } }
这个等价关系怎么找呢,可以这样想;还是拿电影院举例子,就假设我们前面那排经过更前面的回传,已经知道自己是第几排了,那我们是第几排呢,很简单,就是前面那个人的排数+1;你看,这就是等价关系。
我们再来思考下我们刚才是怎么找到的,我们总是直接假设我们已经知道了经过函数后我们已经知道了前n-1的结果,那我们看一下这第n的结果和那个有什么联系就可以了,不用管前n-1是怎么实现的,就当它实现,直接用它的结果就完事了。当然事情也不总是前n-1项,也有可能会用到前n-2等,就是这么个思想。
就是缩小范围,想象我们已经已经知道小范围里的结果,该怎么获得整体的结果。
套到我们这里,要求前n项和,假设已经知道了前n-1项和,那很明显前n项和 = 前n-1项和 + n;这不,等价关系就找到了。
public int add(int n){ if(n == 1) return 1; //第三要素:函数等价关系; return n + add(n-1); }
以后每次在写递归的时候,都强迫自己去寻找这3个要素。
递归是在方法里调用自己本身,其实这个调用会被压入栈内,然后在这个调用里又会去调用方法,不停的压栈,知道有了那个终止条件,ok,最后一次被调用的方法有结果了,可以返回了,然后上一层调用结束,可以返回了,最后再都回来。所以这是一个有去有回的过程,这也说明了为什么一定要有终止条件,不然的话每一次调用都压入了栈内存,都没有执行结束,会导致栈溢出。
比如下面的例子;
上面就是没有终止条件时发生了栈溢出,我们再来看一下上面的例子里;也就是求前n项和中函数的调用;
斐波那契数列的是这样一个数列:1、1、2、3、5、8、13、21、34....,即第一项 f(1) = 1,第二项 f(2) = 1.....,第 n 项目为 f(n) = f(n-1) + f(n-2)。求第 n 项的值是多少。
1.明确函数功能
函数的功能就是获得第n个数的值。
public int getn(int n){ //1.明确函数功能; }
2.寻找递归终止条件
当n为多少的时候我们能直接得到值呢,显然n=1时值为1,n=2时值为1,这就是终止条件。
public int getn(int n){ //2.寻找递归终止条件; if(n <= 2) return 1; }
3.寻找函数等价关系
假设我们已经知道了n的前一个n-1和前两个n-2的值,那第n个值就得出了,f(n) = f(n-1) + f(n-2)。
public int getn(int n){; if(n <= 2) return 1; //3.函数等价关系; return getn(n-1) + getn(n-2); }
反转单链表。例如链表为:1->2->3->4。反转后为 4->3->2->1。
1.明确函数功能
函数的功能就是将一条链表反转。
public Node reverseList(Node head){ //1.明确函数功能; }
2.寻找递归终止条件
当链表为啥样时我们能直接得到反转的链表呢,显然如果链表为空,或者链表就一个节点就能直接得到了,那这就是终止条件。
public Node reverseList(Node head){ //2.寻找递归终止条件; if(head == null || head.next == null) return head; }
3.寻找函数等价关系
这个函数的等价关系就没有那么好找了,仍然是假设:假设后面长度为n的链表后面n-1已经已经反转完了,(有人会问:为什么不能是前面n-1已经反转完了呢?想一下,如果前面n-1已经反转完了,那最后一个节点就找不到了啊,因为最后一个节点之前的节点指到别处去了,那最后一个节点我们获取不到了。如果后面n-1个已经反转完了,第1个节点还指向第二个节点),那我们该怎么做呢。只需要将第二个节点指向第一个,然后第一个指向null就可以了。
public Node reverseList(Node head){ if(head == null || head.next == null) return head; //3.寻找函数等价关系,缩小范围; Node newList = reverseList(head.next);//后面的n-1反转完毕; head.next.next = head; //第二个节点指向第一; head.next = null; return newList; }
输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。
1.明确函数功能
函数的功能就是求一颗二叉树的深度。
public int maxDepth(TreeNode root){ //1.明确函数功能; }
2.寻找递归终止条件
当一颗树是什么样的时候我们能直接得到其深度呢,很显然,当二叉树为空的时候,那深度直接就是0了,那比如说树只有一个节点,那深度不是1也可以直接得到吗?对是可以的,所以说递归的终止条件不是唯一的。
public int maxDepth(TreeNode root){ //2.寻找递归终止条件; if(root == null) return 0; }
3.寻找函数等价关系
这个关系怎么找呢,继续假设,缩小范围:这么一个树上,我们可以先知道左子树和右子树上的深度,那整个树的深度就是左子树和右子树里深度大的那个+1;你看,这不就得到了。就是缩小范围,想象我们已经已经知道小范围里的结果,该怎么获得整体的结果。
public int maxDepth(TreeNode root){; if(root == null) return 0; //3.缩小范围,寻找等价关系; return Math.math(maxDepth(root.left),maxDepth(root.right))+1; }
函数的功能就是求先序遍历二叉树。
public void inOrder(TreeNode root){ //1.明确函数功能; }
2.寻找递归终止条件
当一颗树为空的时候我们能够直接遍历出结果。
public int inOrder(TreeNode root){ //2.寻找递归终止条件; if(root == null) return; }
3.寻找函数等价关系
先序遍历,我们对树的遍历顺序是中左右,所以可以先打印根节点,再缩小范围,先序遍历根节点的左子树,中序遍历根节点的右子树。
public int inOrder(TreeNode root){; if(root == null) return 0; //3.缩小范围,寻找等价关系; System.out.println(root.val); inOrder(root.left); inOrder(root.right); }
函数的功能就是求二叉树的左叶子节点之和。
public void sumOfLeftNode(TreeNode root){ //1.明确函数功能; }
2.寻找递归终止条件
当节点为空时输出值为0。
public int sumOfLeftNode(TreeNode root){ //2.寻找递归终止条件; if(root == null) return 0; }
3.寻找函数等价关系
这种题我们肯定是要遍历二叉树的,所以我们可以先序遍历二叉树,然后就需要判断了,判断当前遍历的节点也就是当前这棵树的是否有左叶子节点(两个条件:左子树,叶子节点),如果是,那就把它加进我们的值,这就是在递去的过程中解决问题,如果不是,再缩小范围,判断下面的左右子树。
int sum = 0; public int sumOfLeftNode(TreeNode root){; if(root == null) return 0; //3.缩小范围,寻找等价关系; if(root.left != null && root.left.left != null && root.left.right != null){ sum += root.left.val; } sumOfLeftNode(root.left); sumOfLeftNode(root.right); }
递归
对递归好的理解