初学者如何理解递归

Posted lmxz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了初学者如何理解递归相关的知识,希望对你有一定的参考价值。

0 递归的定义

如果你没明白递归的定义,参见本文"0.递归的定义"

 

1 从斐波那契数列开始

斐波那契的递推公式

技术图片

斐波那契数列递归算法和递推公式类似

int fibo(int x)
{
    if (x<3) return 1;
    return fibo(x-1)+fibo(x-2);
}

就这么简单?没错,通过这个例子可以看出,递归函数只需要写两部分,一个是递归终止条件(if(x<3) return 1;),一个是递归的“交接”(return fibo(x-1)+fibo(x-2);)

然而这个递归有一点问题,时间复杂度是O(fibo(n))的,因为实际上这个算法每次调用到递归终止条件,都只会给答案加1,如果要计算fiibo(n)则必须一个一个地加,也就是说要加fibo(b)次

问题出在哪里呢?

这是fibo(6)的递归树可以发现fibo(1)被调用了3次,fibo(2)被调用了5次,fibo(3)被调用了3次...

看起来没毛病,实际上不难发现对于同一个数x,只要计算过一遍,就没必要再算第二遍,这就是这个算法浪费时间的地方

技术图片

 

 

 

2.优化

int ans[202];
int fibo(int x)
{
    if (ans[x]!=-1) return ans[x];//如果已经算过一遍,则无需再次计算
    if (x<3) return 1;//递归初始条件
    ans[x]=fibo(x-1)+fibo(x-2);//算出来以后,储存运算结果
    return ans[x];
}
int main()
{
    memset(ans,-1,sizeof(ans));//用-1初始化数组,表示没有算过
    
    return 0;
}

花一点内存作为代价,换来的是O(n)的时间复杂度

技术图片

 

 

 加方框表示已经算过一次的,可以直接出结果,无需继续递归

把已经算过的数据“记住”,下一次再次使用的时候可以直接读取“记忆”,这就是所谓的记忆化

3.数字三角形

传送门:

https://www.luogu.com.cn/problem/P1216

这是动态规划入门题目

设dp[x][y]为从第x行第y列走到底部的最优解

val[x][y]表示第x行第y个数

如果x不等于n

dp[x][y]=

val[x][y]+max(dp[x+1][y],dp[x+1][y+1])//递归“交接”,在动态规划里叫做状态转移

如果x=n

dp[x][y]=val[x][y]//递归终止条件

 

不难写出递归算法

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>

const int N=1003;

int val[N][N],dp[N][N];
int n;
int solve(int x,int y)
{
    if (dp[x][y]!=-1) return dp[x][y];
    if (x==n) return val[x][y];
    int next_max=std::max(solve(x+1,y),solve(x+1,y+1));
    dp[x][y]=val[x][y]+next_max;
    return dp[x][y];
}
int main()
{
    memset(dp,-1,sizeof(dp));
    scanf("%d",&n);
    for (int i=1;i<=n;i++)
        for (int j=1;j<=i;j++) scanf("%d",&val[i][j]);
    int ans=solve(1,1);
    printf("%d",ans);
    return 0;
}

4.汉诺塔

在印度,有这么一个古老的传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。

不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片,一次只移动一片,不管在哪根针上,小片必在大片上面。当所有的金片都从梵天穿好的那根针上移到另外一概针上时,世界就将在一声霹雳中消灭,梵塔、庙宇和众生都将同归于尽。

好吧...不得不说这个故事很蛋疼

问题来了,假设移动一次需要一秒钟,那么从开始移动到世界毁灭,需要多少时间?

我们首先探讨一下如何毁灭世界

先蛋疼地把上面的63片从第一根针移动到第二根针(不管中间过程,只关心结果:63片金片移动到了第二根针上)

现在最大的那一片已经在最上面了,直接把它拿到第三根针上就可以了

然后再无比蛋疼地把那63片从第二根针上移到第三根针上(依然不用管中间过程),于是世界就这么蛋疼地毁灭了(Boom!)

下一个问题就是如何移动那63片

先把最上面62片从第一根针移到第三根针

然后把第63片移到第二根针

再把62片从第三根针移到第二根针

不难看出,只要知道如何移动n片,就可以知道如何移动n+1片

如果n=1,那就无需继续递归,可以直接移动

设f(n)是移动n片所需的步数

那么f(n)=f(n-1)*2+1

f(n-1)*2是因为要把n-1片移动到一个“辅助针”上

+1是因为要移动第n片一次

利用高中的知识(也可以找规律),就可以有递推公式得出通项公式

f(n)=2n-1

f(64)=264-1=18446744073709551615

程序员对这个数应该不陌生,刚好是unsigned long long 的上限(这让我想起来上帝是个程序员的理论)

5.聪明的学生

一个叫兽,有三个学生,而且三个学生均非常聪明!
一天叫兽给他们出了一个题,叫兽在每个人脑门上贴了一张纸条并告诉他们,每个人的纸条上都写了一个正整数,且某两个数的和等于第三个!(每个人可以看见另两个数,但看不见自己的)
叫兽会轮流询问三个人是否已经确定自己头上的数字

当有学生已经确定自己头上的数字时,游戏结束


问题就是,给出三个学生头上的数,求叫兽询问多少次的时候游戏结束

不妨站在学生丙的角度考虑

你现在能看见另外两个同学的数字,并且认为自己的数字有两种情况

假设甲和乙头上的数字分别是3和6,那么你头上的数字就可能是3或9(6-3或6+3)

叫兽问甲,你知不知道你头上的数字?

他说,不知道

叫兽又问乙,乙也说不知道

这时候你就在想,如果你头上的数字是3,那么乙就会看到两个3,由于每个人头上的数字都是正整数,不可能是零,那么乙就可以确定他是6

但乙并没有确定,说明你头上的数字不是3,是9

于是当叫兽问你的时候,你说你确定你头上的数字是9

丙之所以可以知道自己的数字,是因为他假设的错误情形会在第二次询问时结束,但第二次询问的时候还没有结束,于是并排除了错误的情况

纵观全局,三个人都会假设两种情况,其中一种是与实际情况一致的,另一种是与实际情况不一致的

当且仅当一个人的错误假设被排除,游戏结束

如果计算出三个人的错误假设的结束时间,就可以得出游戏结束的时间

如何计算三个人的错误假设的结束时间?递归!

递归终止条件:如果一种情况下,两个人头上的数字一样,那么那个数字与另外两个人不一样的人第一次被询问时就会确定自己的数字

以上是关于初学者如何理解递归的主要内容,如果未能解决你的问题,请参考以下文章

java中递归函数中的字符串返回值

精心收集的 48 个 JavaScript 代码片段,仅需 30 秒就可理解!(转载)

vue递归组件的一些理解

如何理解这段代码片段中的两对括号?

浅谈对递归算法的理解……

如何真正的理解递归?