用递归调用法求斐波那契函数_递归入门:三要素解决递归

4c02c3116c808637b7dd350f5a4034cd.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

熟练的话,你应该知道,要让后一项的next指向前一项。

下图是递到最后之后,要归的样子,是容易想象出来的。

1f5fdf5c57008fbacfeb86faea16952d.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

这时只要把2的next指向1,1的next指向null就行了,问题变得直观明了了。即通过改变newList 链表之后的结果如下:

2be4d6ba05977ae3c70fe615680be908.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 8deae4e2b54f1541d94831149bc67a8c.png e53d24dd2f3a9438758fd8683d7704da.png

 文案:叶锐坚   

排版:叶锐坚   邱彦椋

▇ 扫码关注我们 ▇

我知道你在看

258b18729344e59a8a25187b39aee668.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值