对递归问题的一些思考

递归问题

递归是一个非常实用的算法思路,递归题更是比赛中经常会考到的,它的简单粗略的定义就是函数不断调用自身,把问题范围缩小,当直接可以得到边界数据的时候(递归边界),再返回来求出对应的解。
为了便于理解,举一个阶乘例子:
首先我们要实现递归,就必须考虑两个条件:
1.递归边界,也就是我们上面所说的可以直接得到的数据,这是递归的出口,很重要,如果没有递归边界,这个递归就等同与死循环,程序会直接崩溃。我们要求n!,那我们要想递归边界一般是特殊的数据,是可以直接得出来的,这是我们发现0!是等于1的,而这个就是我们的递归边界。
2.递归式,递归式是我们整个递归的核心,递归式完成了将整个大问题拆分成无数个小问题,如要找到n!的递归式,我们可以发现n! 可以分成若干个 (n-1) x n(n! = 1 x 2 x 3 x 4…x n),而这个(n-1) x n就是我们的递归式。
综上,我们可以得出代码

#include<cstdio>
using namespace std;
int f(int n){
//递归边界0!= 1
if(n==0)return 1;
//递归式(n-1)*n
return n*f(n-1);
}

int main(){
int n;
while(scanf("%d",&n)!=EOF){
     printf("%d\n",f(n));
}
return 0;
}

为了便于理解,这里有一张整个递归执行的模型图,以求5的阶乘为例
在这里插入图片描述
由图可见,计算机在执行递归的时候还是进行了很多运算的,从时间复杂度来看,递归并没有使我们的代码更加有效率,对于有些问题甚至可能会更慢,但是它是代码更加简洁,减少了 代码的冗余度,在实际情况中还是很实用的。所以接下来,我们来讨论一下递归的时间复杂度的优化问题。
对于递归的时间复杂度问题,无非是计算机在处理递归时会直接按照递归式一层一层展开来,一些一样的数据会被重复运算很多次,这样就导致了我们的递归的时间复杂度会很高。举个例子,如计算经典问题Fibonacci时(Fibonacci数列是满足f(0)=f(1)=1,f(n) = f(n-1)+f(n-2)的数列),如果使用递归,见代码:

#include<cstdio>
using namespace std;
int f(int n){
    //递归边界f(0) = f(1) = 1
if(n<=1) return 1;
//递归式
return f(n-1)+f(n-2);

 }
int main(){
int n;
while(scanf("%d",&n)!=EOF){
     printf("%d\n",f(n));
}
return 0;
}

一样我们画出计算机执行递归时的草图,如计算f(10):
在这里插入图片描述
我们只画了两层递归,就出现好几个重复的数据(f(7),f(8)),计算机在处理这些重复数据时,还是会一丝不苟地重新计算,时间效率自然就低了。所以如果我们如果用一个数组把这些算过的数据的答案存起来,当计算机再次碰到这些数据就直接得到答案,不需要再次去计算,这样程序就得到优化了。见代码:

#include<cstdio>
using namespace std;
const int maxn = 101;
int a[maxn];
int f(int n){
  if(n<=1)return 1;
  if(a[n]!=0)return a[n];
  return a[n] = f(n-1)+f(n-2);
}


int main(){

int n;
while(scanf("%d",&n)!=EOF){
    printf("%d\n",f(n));
}
return 0;
}

我们通过a数组将计算过的数据答案保存起来,如果再次碰到这个值就直接返回答案,很明显这种写法计算机执行起来会变得更快。

我们再来讨论一个蓝桥杯上的经典递归例子—母牛的故事
母牛的故事
在这里插入图片描述
这道题思路很简单,你只要按照它的要求慢慢罗列每年牛的数量,你就会发现规律,对于具体的思考过程不是我们今天讨论的重点,就不再详述。很多小伙伴会发现这道问题的规律是,前四年母牛的数量等于年数(母牛每年增长一头),到了第五年,大母牛生的小母牛开始成熟了也可以生小母牛,第五年牛的数量等于第四年牛的数量加上第二年小母牛的数量(它们已经长成大母牛了)。所以可以弄清楚,递归边界是f(n) = n(n<=4);递推式是f(n) = f(n-1)+f(n-3)。当然想到这儿,这道题应该是完成了吧,先把代码码上

#include<cstdio>
using namespace std;
int f(int n){
    //递归边界
  if(n<=4)return n;
    //递归式
  return f(n-1)+f(n-3);
}


int main(){

int n;
while(scanf("%d",&n)!=EOF&&n){
    printf("%d\n",f(n));
}
return 0;
}

如果你直接拿这段代码去c语言网上的0J提交,会直接给你报一个运行时间超限的错误,这归根结底就是我们上面所讲的时间效率低的问题,这是不能AC的。那我们直接使用我上面所讲的思路

#include<cstdio>
using namespace std;
const int maxn = 101;
int a[maxn];
int f(int n){
  if(n<=4)return n;
  if(a[n]!=0)return a[n];
  return a[n] = f(n-1)+f(n-3);
}


int main(){

int n;
while(scanf("%d",&n)!=EOF&&n){
    printf("%d\n",f(n));
}
return 0;
}

这种就能直接通过了,另外除了递归的优化做法外,这种时间超限的问题,我们也可以尝试打表法来做,所谓打表法就是先让计算机把可能的答案列出来(打表的操作是不算在程序运行时间内的),当然也是需要数组的,然后再让计算机根据条件把正确的答案选出来就行了,这也是十分有用的方法,也推荐大家这样做。
打表法AC代码:

#include<iostream>
using namespace std;
//101已经涵盖了0J的测试范围
const int maxn = 101;
int main(){
 int n;
 int a[maxn];
 while(cin>>n && n){
    //进行打表
    for(int i = 1;i<=101;i++){
        if(i<=4){
          a[i] = i;
   }
   else{
         a[i] = a[i-1]+a[i-3];
   }
}    
     //直接打印对应的答案
     printf("%d\n",a[n]);
 }
return 0;
}

递归真的很重要,建议大家多多练习和掌握。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值