您的位置:首页 > 其它

【算法】递归

2021-07-26 21:50 661 查看

递归

1. 概念

大师 L. Peter Deutsch 说过:To Iterate is Human, to Recurse, Divine.中文译为:人理解迭代,神理解递归。

通俗的讲:函数自己调自己;

再举一例子:比如说在电影院,你想知道自己在第几排,但是人太多黑咕隆咚的你也不能去数,怎么办呢,你就问前面那个人你在第几排,前面的和你一样也问前面的,这样到第一排了,他知道自己在第一排,然后又回传给第二个人,一直回传最后你就知道自己在第几排了。

递归的内涵:递归是一个有去有回的过程。

  • “有去”是指:递归问题必须可以分解为若干个规模较小,与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决。
  • “有回”是指:这些问题的演化过程是一个从大到小,由近及远的过程,并且会有一个明确的终点(临界点),一旦到达了这个临界点,就不用再往更小、更远的地方走下去。最后,从这个临界点开始,原路返回到原点,原问题解决。 

2. 三要素

想办法写出一个递归程序需要明确这里面的三要素;

  • 1.明确函数功能
  • 2.寻找递归终止条件
  • 3.找出函数等价关系,缩小问题规模

2.1 明确函数功能

对于递归,首先不用管函数里面的代码应该是什么样的,而是我们首先明白这个函数是做什么的,能完成什么功能。
比如说我们要让求一个n的前n项和。我们首先写出了这个函数:

public int add(int n){
//那到现在我们先不用管这里面怎么写了
//我们首先明白我们这个函数调用它,给它传个n进去就能得到1到n的和。
}

2.2 寻找递归终止条件

  函数自己调自己总不能一直调下去吧?想一下上面的例子,如果电影院根本没有头,那你一直在等着,答案也不可能回传回来,所以一定要有一个终止条件,然后我们就能把结果返回回来。
这个终止条件的意思是:我们必须根据这个参数值,能直接得到我的功能结果。
  比如我们想知道自己在第几排,我们能够根据前面没有人了,直接得到我就在第一排;比如我们想求前n项和,我们能够根据n为1的时候,直接得到和就为1;
  所以我们可以把上面代码补上第二要素;

public int add(int n){
//第二要素:我们能够根据n为1直接得到前n项和为1.
if(n == 1) return 1;
}

2.3 找出函数等价关系,缩小问题规模

  第三个要素就是我们要找出函数的等价关系,不断的缩小问题规模
  其实在寻找等价关系,求解递归问题的时候右两种模型,想一下我们这个递归的过程,有去有回,所以可以在递去的过程中解决问题,亦可以在归来的时候解决问题,这就产生了两种模型。
模型一:在递去的过程中解决问题
  这类基本上适用于没有明确的等价关系,而更多的时候是一种过去的时候就可以输出,比如打印,遍历之类的,或者说需要有判断条件的时候,在过去的时候进行判断,满足就可以左一些处理等。这类问题上,比如说二叉树的遍历,二叉树的左叶子节点之和等。

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个要素。

3. 栈内存

递归是在方法里调用自己本身,其实这个调用会被压入栈内,然后在这个调用里又会去调用方法,不停的压栈,知道有了那个终止条件,ok,最后一次被调用的方法有结果了,可以返回了,然后上一层调用结束,可以返回了,最后再都回来。所以这是一个有去有回的过程,这也说明了为什么一定要有终止条件,不然的话每一次调用都压入了栈内存,都没有执行结束,会导致栈溢出。
比如下面的例子;

上面就是没有终止条件时发生了栈溢出,我们再来看一下上面的例子里;也就是求前n项和中函数的调用;

4. 举例

4.1 斐波那契数列(模型二)

斐波那契数列的是这样一个数列: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);
}

4.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;
}

4.3 求二叉树深度(模型二)

输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。

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;
}

4.4 遍历二叉树(模型一)

函数的功能就是求先序遍历二叉树。

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);
}

4.5 求二叉树左叶子节点之和(模型一)

函数的功能就是求二叉树的左叶子节点之和。

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);
}

参考链接

递归
对递归好的理解

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: