文章目录
- 一、递归
- 1.递归的概念
- 2.递归的思想
- 3.递归的限制条件
- 二、递归的一些典型例子
- 1.求一个数的阶乘
- 2.顺序打印一个整数的每一位
- 3.汉诺塔
- 4.青蛙跳台阶
- 5斐波那契数列
- 递归和迭代的对比
一、递归
1.递归的概念
递归是学习C语言函数绕不开的一个话题,那什么是递归呢? 递归其实是一种解决问题的方法。在C语言中,递归就是函数自己调用自己。
2.递归的思想
递归就是:把一个大型复杂的问题层层转化为一个与原问题相似的,但规模较小的子问题来求解。直到子问题不能再被拆分,递归就结束了。所以递归的思想就是把大事化小的过程。递归中的递就是递推的意思,归就是回归的意思。
3.递归的限制条件
递归在书写的时候,有2个必要条件:
● 递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。
● 每次递归调用之后越来越接近这个限制条件。
二、递归的一些典型例子
1.求一个数的阶乘
#include <stdio.h>
int fac(int n)
{if (n == 1||n == 0)return 1;elsereturn n * fac(n-1);
}
int main()
{int n = 0;scanf("%d", &n);int ret = fac(n);printf("%d的阶乘是%d\n",n,ret);return 0;
}
运行结果:
上面通过函数递归来求一个正整数的阶乘,那具体要怎么理解上面的代码呢?分析:一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,即n的阶乘公式为 n=n*(n-1)! 并且0的阶乘为1。自然数n的阶乘写作 n!。例如:
5! = 5*4*3*2*1
4! = 4*3*2*1
所以 5! = 5*4!
这样的思路就是把一个较大的问题,转换为一个与原问题相似,但规模较小的问题来求解的。当n==1或者n==0的时候,n的阶乘是1,其余n的阶乘都是可以通过公式计算。
那我们就可以写出函数fac求n的阶乘,假设fac(n)就是求n的阶乘,那么fac(n-1)就是求n-1的阶乘。所以构造的函数就是上面的fac。
就是因为递归存在限制条件,这就使得函数自己调用自己时,会有结束的时候。当一个复杂的问题被拆解到不能再拆解的子问题时,我们一眼就能看出子问题的答案。而求解出子问题的答案后,我们就能逐一求解上一层的子问题,以至于就能求解出原问题的答案了。
2.顺序打印一个整数的每一位
输入一个整数n,按照顺序打印整数的每一位。比如:
1.输入: 1234,输出:1 2 3 4
2.输入: 520,输出:5 2 0
#include <stdio.h>
void Print(int n)
{if (n > 9)Print(n / 10);printf("%d ", n % 10);
}
int main()
{int n = 0;printf("请输入一个整数:");scanf("%d", &n);Print(n);return 0;
}
运行结果:
这里需要知道一个知识点:在C语言中,整数除以一个整数,得到的还是整数,因为小数部分被丢弃掉了。
①在C语言中,一个整数除以10以后,这个整数的个位会被去掉,得到个位前面的整数部分。比如:327/10得到结果是32,去掉了个位上的7。一个整数除以10以后,位数会减少一位。
②一个整数模(%)上10以后得到的是个位上的数字,比如:43%10得到的结果为3,即余数就为3。
那怎么理解上面的代码呢?
对于这个函数,如果传进去的实参为1234,进入函数就先判断n是否大于9,如果n大于9,就将n/10传给Print函数,这里就又调用了Print函数,直到n的值小于等于9以后,if语句不成立了,就执行printf("%d " , n%10)这条语句,即先打印1234的最高位1,然后返回上一层继续打印百位的2,然后再返回上一层打印十位上的3,最后返回第一层打印个位上的4。
图解释:
3.汉诺塔
先学习一下什么是汉诺塔(河内塔)? 汉诺塔是一个起源于印度古老传说的益智游戏,由法国数学家爱德华·卢卡斯于1883年发明。
汉诺塔的玩法:对于上面的三个木桩,中间的木桩上叠放有圆盘,而且是按照小圆盘在大圆盘上方的次序叠放的。玩法是将一个木桩上的圆盘移动到另一个木桩上。
移动规则:
● 1.一次只能移动一个圆盘
● 2.每个木桩上只有最顶层的圆盘可以移动,并且所移动的圆盘只能移动到空木桩上或者是它要比木桩顶层已存在的圆盘小。也就是说,你每移动一次圆盘,不管在哪根木桩上都要保证小圆盘在大圆盘的上方。
我们从最简单的情况开始逐一讲解:
Ⅰ.如果木桩上只有一个圆盘的时候,要把A柱上的圆盘移动到C柱上,直接拿过去就好了。A→C
Ⅱ.如果A木桩上有两个圆盘,现在要把A木桩上的圆盘移动到C木桩上,就需要借助B桩移动三次圆盘。
共需三步:A→B,A→C,B→C
Ⅲ.如果A木桩上有三个圆盘,现在要把A木桩上的圆盘移动到C木桩上最少需要移动多少次圆盘呢?
答案是最少需要7步:A→C,A→B,C→B,A→C,B→A,B→C,A→C
到第四步完以后发现,最大的那个圆盘已经放在C柱上了,剩下的两个圆盘要放到C柱上,其实就和上面只有两个圆盘的情况是一样的了,只是这里要借助A木桩移动到C木桩上,位置不一样,但是移动的次数和两个圆盘情况下是一样的。上面的4步加上只有两个圆盘情况下的3步,最少只需要7步就能把A柱上的圆盘移动到C柱上。
其实通过上面的1~3层圆盘的汉诺塔移动情况的分析,不难发现这里就有递归的思想。像只有两个圆盘的情况:我们要把A柱上最大的那个圆盘先移动到C柱上,就要先把A柱上较小的圆盘先转移到B柱上,然后才能把较大的那个圆盘移动到C柱上。然后你会发现剩下的那个较小的圆盘要移动到C柱上,就跟柱子上只有一个圆盘的情况是一样的,只需要移动一步即可。同理:A柱上如果有三个圆盘的情况,当把最大的那个圆盘移动到了C柱上以后,剩下的两个较小圆盘要移动到C柱上,移动的次数就跟柱子上只有两个圆盘的情况是一样的,只是借助的柱子不一样而已。那代码要怎么实现呢?
#include <stdio.h>
//1.构造一个用来打印移动轨迹的函数
void Print_Movetrack(char ori, char des)
{static int time = 0;//定义一个静态变量,用来记录移动的次数printf("第%d步:%c-→%c\n", ++time, ori, des);
}
//2.汉诺塔代码的递归逻辑
void Hanoi(int n, char a, char b, char c)
{if (n == 1){Print_Movetrack(a, c);}else{Hanoi(n - 1, a, c, b);//Ⅰ将A柱上最大圆盘上方的n-1个圆盘借助C柱移动到B柱上Print_Movetrack(a, c);//Ⅱ将最大的圆盘从A柱移动到C柱上Hanoi(n - 1, b, a, c);//Ⅲ将刚才移动到B柱上的圆盘借助A柱移动到C柱上}
}
int main()
{int n = 0;printf("请输入圆盘个数:");scanf("%d", &n);Hanoi(n, 'A', 'B', 'C');//n是圆盘个数;A,B,C是三根木桩的编号return 0;
}
运行结果:
最难理解的是这个函数是怎么构造出来的,递归的逻辑是什么?但是也紧扣递归的限制条件,函数里面有使这个递归结束的限制条件,每一次递归,都会越来越接近这个限制条件。
我们以A柱上有3个圆盘的情况来说明上面代码的递归逻辑:函数在逐层调用自己时,当满足限制条件后,会逐一返回上一层继续执行上一层后续的代码。
4.青蛙跳台阶
青蛙跳台阶问题是啥?这个问题描述的是:一只青蛙去跳台阶,它一次可以跳1个台阶,一次也可以跳2个台阶。
问:如果现在有n层台阶,青蛙要跳上这n层台阶,共有多少种跳法?
分析:
Ⅰ.当只有一层台阶(n=1)的时候,青蛙只能跳1个台阶,只能跳一次。如图所示:
Ⅱ.当有两层台阶(n=2)的时候,青蛙要跳上这两层台阶,共有2种跳法。一是每次跳1个台阶,共两次跳完;二是一次跳2个台阶,一次就跳完。如下图:
Ⅲ.当有三层台阶(n=3)的时候,要怎么算有多少种跳法呢?首先分析一下:青蛙一开始一次,要么跳1个台阶,要么跳2个台阶。
①如果青蛙一开始一次跳了1个台阶,那么就还剩下两层台阶,那这两层台阶的跳法,就跟上面只有两层台阶(n=2)的跳法是一样的,共有2种。
②如果青蛙一开始一次跳了2个台阶,那么就还剩下一层台阶,这一层台阶的跳法就跟只有一层台阶(n=1)的跳法一样,只有一种。
所以综上所述,三层台阶的跳法总共有3种。就等于1层台阶和2层台阶的跳法总和。如下图所示:
Ⅳ.当有四层台阶(n=4)的时候,也是看青蛙一开始是怎么跳的,如果一开始青蛙跳了1个台阶,那后面就还剩三层台阶,就跟上面只有三层台阶(n=3)的跳法是一样,有3种跳法。如果一开始青蛙跳了2个台阶,就还剩下两层台阶,跟上面只有两层台阶(n=2)的跳法一样,有2种跳法。所以对于四层台阶,总共有5种跳法。就等于2层台阶和3层台阶的跳法总和。
Ⅴ.依次类推,如果青蛙要跳上n层台阶,这n层台阶的跳法就等于(n-2)层台阶和(n-1)层台阶的跳法总和。规律就是:
C语言代码的实现:
#include <stdio.h>
int Step(int n)
{if (n == 1)return 1;else if (n == 2)return 2;elsereturn Step(n - 2) + Step(n - 1);
}
int main()
{int step_num = 0;printf("请输入台阶层数:");scanf("%d", &step_num);printf("%d层台阶共有%d种跳法\n",step_num , Step(step_num));
}
运行结果:
我们把逐层台阶的跳法数量整理出来看:
通过上面的表也可以直观的看出来,青蛙跳台阶的种数算法怎么计算,第n层台阶跳法就等于n-2层台阶和n-1层台阶的跳法总和。
5斐波那契数列
先介绍一下斐波那契数列:
斐波那契数列又称黄金分割数列,是由意大利数学家莱昂纳多·斐波那契以兔子繁殖为例而引入的,故称为兔子数列。这个问题是:兔子在出生两个月以后,就有繁殖能力了,成熟以后的一对兔子每个月都能生出一对小兔子,如果所有的兔子都不死,那么问在一年以后可以繁殖多少对兔子?
我们拿一对刚刚出生的兔子来分析:
①刚出生的一对小兔子第一个月还没有繁殖能力,所以第一个月是一对兔子。
②两个月以后生下一对小兔子,所以现在共有两对小兔子。
③第三个月后,老兔子又生下一对兔子,上个月新生的那一对兔子还没有繁殖能力,所以现在一共有3对兔子。
④第四个月后,老兔子继续生下一对小兔子,刚刚成熟的一对兔子也生下一对小兔子,加上老兔子上个月刚出生的一对兔子现在一共就有5对兔子。
…
画出过程图来说明:
上图经过月份那里有一个0,你可以理解为兔子刚出生的时刻。注意图中写的是经过的月份,不是实际月份,不要理解错误。所以通过上图可以直观的看出兔子繁殖对数的规律:
由表可以看出从第三月起,每个月的兔子对数是前两个月的兔子对数之和。由此给出斐波那契数列的定义:一个数列从第三项起,每一项都等于前两项之和,即1 1 2 3 5 8 13 21…这样一个数列。那递归的逻辑在这里就可以理解了,那求第n个斐波那契数的代码实现:
#include <stdio.h>
//斐波那契数的递归逻辑
int Fib(int n)
{if (n == 1 || n == 2)return 1;elsereturn Fib(n - 2) + Fib(n - 1);}
int main()
{int n = 0; //这个n表示的是斐波那契数列中的第几项scanf("%d", &n);printf("第%d项斐波那契数为%d\n", n , Fib(n));return 0;
}
运行结果:
所以在第12个月的时候(还没有经过十二月),总共有144对兔子,即288只兔子。如果是一年以后,那就要经过十二月,到下一年的一月开头,那就有233对兔子,即有466只兔子。
递归和迭代的对比
上面的青蛙跳台阶和斐波那契数列是非常相似的,但是对于斐波那契数列是不适合使用递归的方法来实现。当我们要求的斐波那契数列的项数很大时,比如我们要求第50项斐波那契数时,这个递归所花费的时间是非常长的,为什么呢?看下面的递归逻辑图:
从上图可以看出,递归程序会不断的展开,在展开的过程中,我们很容易就能发现,在递归的过程中会有重复计算,而且递归层次越深,冗余计算就会越多。如果采用非递归的方式(迭代),效率就可以大大提高:
#include <stdio.h>
int Fib(int n)
{int a = 1;int b = 1;int c = 1;while (n > 2){c = a + b;a = b;b = c;n--;}return c;
}
int main()
{int n = 0;while (scanf("%d", &n) != EOF){int ret = Fib(n);printf("第%d项斐波那契数为%d\n", n, ret);}return 0;
}
运行结果:
上面的这种求第几项斐波那契数的方法就是迭代法。每一次对过程的重复称为一次"迭代",而每一次迭代得到的结果将会作为下一次迭代的初始值。这种就叫做迭代法,也称为辗转法。所以不是所有的问题都适合用递归的方法。
Ⅰ.递归的优缺点
优点:
● 可以将大问题转化为小问题,减少代码量。
● 可以去掉不断重复的代码,使代码精简,提升可读性。
缺点:
● 递归调用浪费空间,递归太深还容易造成堆栈溢出。
Ⅱ.迭代的优缺点
优点:
● 可以将重复的问题转化为一单问题的重复操作,减少代码量。
● 代码运行效率高,时间只因为循环次数的增加而增加,没有额外的内存空间开销。
缺点:
● 代码不如递归简洁,有时可能不容易理解。