![4c02c3116c808637b7dd350f5a4034cd.png](https://i-blog.csdnimg.cn/blog_migrate/247fc46f765c9d10d79a1ebc13fe09f8.png)
点击蓝字关注我们
相信大家刚开始接触递归时都感受到了递归的奇妙,但实际做题,写进代码的时候却不知道该怎么用,有时候还容易给递归搞晕。今天跟大家分享一下在博客上看到的递归三要素,并运用在几个简单的案例上。
自己调用自己的函数就是递归函数,为什么要自己调用自己呢?在算法领域,有种思想叫分治思想,它的内容主要是把一个大的问题拆成若干的小(子)问题,当小问题都解决了,那么整体的大问题也就解决了。我们发现,处理小问题和处理大问题时,他们只是规模不一样,但是处理的方式都完全一样。这种分治思想就是递归函数最初的思想。
下面开始介绍解决递归函数的三要素
递归三要素:
第一要素:明确你这个函数要干什么
首先要明确这个函数的功能是什么,他要完成什么样的一件事。我们先不管函数里面的代码什么,而是要先明白,你这个函数是要用来干什么。
例如,我定义了一个函数
// 算 n 的阶乘(假设n不为0)
int f(int n){
}
这个函数的功能就是算n的阶乘了。好了,我们定义了一个函数,而且定义了它的功能是什么,接下来看第二要素
第二要素:寻找递归结束条件
所谓递归,就是会在函数内部代码中,调用这个函数本身,所以,我们必须要找出递归的结束条件,不然的话,会一直调用自己,进入无底洞。也就是说,我们需要找出当参数为啥,或满足什么条件时,递归结束,之后把结果返回,请注意,这个时候我们必须能根据这个参数的值,能够直接知道函数的结果是什么。
例如,上面那个例子中,易得有结束条件,f(1) = 1。完善我们函数内部的代码,把第二要素加进代码里面,如下
// 算 n 的阶乘(假设n不为0)
int f(int n){
if(n == 1)
return 1;
}
当然,你也可以一眼看出f(2)=2,n=2也可以是递归结束的条件。只要你觉得参数是什么时,你能够直接知道函数的结果,那么你就可以把这个参数作为结束的条件。
所以为了更加严谨,我们可以写成这样:当 n <= 2时,f(n) = n,
// 算 n 的阶乘(假设n不为0)
int f(int n){
if(n <= 2)
return n;
}
第三要素:缩小范围,找出函数的等价关系式
第三要素就是,我们要不断缩小参数的范围,缩小之后,我们可以通过一些辅助的变量或者操作,使原函数的结果不变。
例如,f(n) 这个范围比较大,我们可以让 f(n) = n * f(n-1)。这样,范围就由 n 变成了 n-1 了,范围变小了,并且为了原函数f(n) 不变,我们需要让 f(n-1) 乘以 n。
说白了,就是要找到原函数的一个等价关系式,f(n) 的等价关系式为 n * f(n-1),即f(n) = n * f(n-1)。
这个等价关系式的寻找,可以说是最难的一步了,还需要多接触题目。
// 算 n 的阶乘(假设n不为0)
int f(int n){
if(n <= 2)
return n;
// 把 f(n) 的等价操作写进去
return f(n-1) * n;
}
至此,递归三要素已经都写进代码里了,所以这个 f(n) 功能的内部代码我们已经写好了。
这就是递归最重要的三要素,每次做递归的时候,你就强迫自己试着去寻找这三个要素。
下面给出几个案例让大家熟悉一下这种感觉
案例1:斐波那契数列
斐波那契数列:1、1、2、3、······满足n>2时f(n)=f(n-1)+f(n-2),求第n项的值是多少
1、递归函数的功能
假设f(n)的功能是求第n项值,代码如下
//求斐波那契数列f(n)的值
int f(int n){
}
2、找出递归结束的条件
显然,当n=1或n=2时,有f(1)=f(2)=1,所以递归结束条件时n<=2时,f(n)=1 。代码如下
//求斐波那契数列f(n)的值
int f(int n){
if(n<=2) return 1;
}
3、缩小范围,找出函数的等价关系式
找题目的等价关系式是最难的,只是这里题目已经给出了,f(n)=f(n-1)+f(n-2),这够容易的了,代码如下
//求斐波那契数列f(n)的值
int f(int n){
if(n<=2) return 1;
return f(n-1)+f(n-2);
}
搞定!继续按着这个模式再看几个案例。
案例2:小青蛙跳台阶
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
1、递归函数的功能
假设f(n)的功能是求青蛙跳上一个n级台阶共有多少种跳法,代码如下
//求青蛙跳上一个n级台阶共有f(n)种跳法
int f(int n){
}
2、找出递归结束的条件
求递归结束的条件,只要把n压到很小很小就行了,n越小,就越容易直观算出f(n)是多少。所以有f(n)=1 可以作为递归结束条件。
///求青蛙跳上一个n级台阶共有f(n)种跳法
int f(int n){
if(n == 1)
return 1;
}
3、缩小范围,找出函数的等价关系式
第一次跳的时候有两种情况
i)跳一层 ,此时还剩下n-1层没跳,剩下的n-1层有f(n-1)种跳法
ii)跳两层,此时还剩下n-2层没跳,剩下的n-2层有f(n-2)种跳法
所以只要两种情况取并集,就能表示出所有情况了,也就是有f(n-1)+f(n-2)种跳法,代码如下
///求青蛙跳上一个n级台阶共有f(n)种跳法
int f(int n){
if(n==1) return 1;
return f(n-1)+f(n-2);
}
代码写完了吗?再检查一遍,f(2)=f(1)+f(0),而f(0)又会调用f(-1)f(-2),程序会陷入死循环。
在这里再次强调要注意递归条件结束是否严谨。递归函数出错的大部分情况就是递归结束条件不够严谨,导致死循环。因此,按照三步走找出等价关系式之后,还得返回第二步根据等价关系式检查结束条件有无遗漏。回到刚才的例子,补充条件后的代码如下
///求青蛙跳上一个n级台阶共有f(n)种跳法
int f(int n){
//f(0)=0,f(1)=1,f(2)=2等价于 n<=2时,f(n) = n。
if(n <= 2){
return n;
}
ruturn f(n-1) + f(n-2);
}
案例3:反转单链表
反转单链表。例如链表为:1->2->3->4。反转后为 4->3->2->1
链表的定义如下
struct Node
{
int data;
struct Node * next;
};
typedef struct Node NODE;
1、定义递归函数的功能
假设f(n)的功能是反转一个链表,代码如下
/*函数返回一个反转了的链表,假设head指向第一个结点*/
NODE * reverseList(NODE * head){
}
2、找出递归结束的条件
当链表只有一个节点,或者是空表,那就直接返回head
/*函数返回一个反转了的链表,假设head指向第一个结点*/
NODE * reverseList(NODE * head)
{
if(head == NULL|| head->next ==NULL)
return head;
}
3、缩小范围,找出函数的等价关系式
对于数来说,数字的范围在不断缩小,对于链表来说,就是链表的节点个数在不断在变小。
如果实在找不出等价关系,就先试试对reserveList(head->next)递归走一遍
![85a754ab8524b9dba47170f7e35c037d.png](https://i-blog.csdnimg.cn/blog_migrate/a9283f64e8bba17466bb2e3a665ee66b.png)
熟练的话,你应该知道,要让后一项的next指向前一项。
下图是递到最后之后,要归的样子,是容易想象出来的。
![1f5fdf5c57008fbacfeb86faea16952d.png](https://i-blog.csdnimg.cn/blog_migrate/d78230757f9914dbe0c5ee0e6bbfbeca.png)
怎么让4指向3呢?每次递归的操作共性就是后一项next指向前一项,我们先不必执着于怎样抽象的表达后一项指向前一项,我们先放一放,从整体看。
缩小范围先对2->3->4也就是head->next递归试试,暂且把递归结果保存起来。代码如下
/*函数返回一个反转了的链表,假设head指向第一个结点*/
NODE * reverseList(NODE * head)
{
if(head == NULL|| head->next ==NULL)
return head;
NODE * newList = reverseList(head->next);
//这里的参数会递归下去 (head->next)->next···
// 我们先把递归的结果保存起来,先不返回,因为我们还不清楚这样递归是对还是错。
}
我们在第一步的时候,就已经定义了 reverseList()函数的功能可以把一个单链表反转,所以,我们对 2->3->4反转之后的结果应该是这样:
![025b95dd992e0634f60814697b56d05c.png](https://i-blog.csdnimg.cn/blog_migrate/695ed3e49d5578885a6498be501655d3.png)
这时只要把2的next指向1,1的next指向null就行了,问题变得直观明了了。即通过改变newList 链表之后的结果如下:
![2be4d6ba05977ae3c70fe615680be908.png](https://i-blog.csdnimg.cn/blog_migrate/96765cf0396e50acdd57973e23f0f335.png)
也就是说 reverseList(head)等价于 reverseList(head->next) + 改变1.2两个节点的方向。这就是等价关系式,实现的代码如下
/*函数返回一个反转了的链表,假设head指向第一个结点*/
NODE * reverseList(NODE * head)
{
//递归结束条件
if(head == NULL|| head->next ==NULL)
return head;
//递归反转子链表
NODE * newList = reverseList(head->next);
//获取2结点,也即head的下一个结点
NODE *p2 =head->next
//让2的next指向1
p2->next = head;
//1的next指向NULL
head->next = NULL;
//把调整之后的链表返回
return newList;
}
说实话刚开始我也想不到可以用递归这样做,刚开始感到有点难是正常的,再多练些就能得想到了。
总结
再复习一遍递归三要素:
一、定义递归函数功能
二、寻找递归终止条件
三、缩小范围,寻找等价关系式
另外,递归还需要考虑优化问题
1、考虑是否重复计算.不进行优化的话,有非常多的子问题是被重复计算的,当n越大,重复的计算就越多,花费的时间就越多。
2、考虑是否可以自底而上,其实也就是递推。对于递归,我们一般都是从上往下递归的,不过有时当n太大时,如n=10000,n要向下递归10000层直到n<=1才返回,栈空间会不够用。
具体的优化方法我们下期再讲,递归挺麻烦的,能不用递归就不用递归[手动狗头]
希望通过三要素做完这几道题能加深你对三要素的理解。刻意的使用、练习递归三要素,或许能帮助你以后做递归题时获得思路。
改编自[帅地学编程]公众号文章:为什么你学不会递归?告别递归,谈谈我的一些经验
https://mp.weixin.qq.com/s/DK6CS0AmWEplNhwEs8mSuQ
已获得转载许可。
![bd146553a2e520ed01309e097bdc6d15.png](https://i-blog.csdnimg.cn/blog_migrate/196ee23cd222361edc21980dacfbe511.jpeg)
![8deae4e2b54f1541d94831149bc67a8c.png](https://i-blog.csdnimg.cn/blog_migrate/dcfe3f889fd9b9241638d4bba5f0ead5.png)
![e53d24dd2f3a9438758fd8683d7704da.png](https://i-blog.csdnimg.cn/blog_migrate/bf55cca718eafd0852dbbf946fc0bac2.png)
文案:叶锐坚
排版:叶锐坚 邱彦椋
▇ 扫码关注我们 ▇
我知道你在看哟
![258b18729344e59a8a25187b39aee668.png](https://i-blog.csdnimg.cn/blog_migrate/3dd1476bf98def537a01a4bfd06e9c4a.png)