递归的实质
我们知道:函数可以调用其他函数实现部分功能,那么,函数可不可以调用自己呢?
答案:当然!
故事
从前有座山,山上有座庙,庙里有两个和尚,老和尚对小和尚说:从前有座山,山上有座庙,庙里有两个和尚,老和尚对小和尚说:从前有座山,山上有座庙,庙里有两个和尚,老和尚对小和尚说:……
怎么用C语言打印出来呢?
简单:不就是循环嘛!
#include<stdio.h>
int main(){
int i=1;
for(i=1;i<=20;i++){
printf("从前有座山,山上有座庙,庙里有两个和尚,老和尚对小和尚说\n");}
return 0;
}
重复打印的事,我们可以交给函数执行:改!
也就是之后无论哪个和尚说这个故事都可以调用这个函数,节省代码量和时间!
#include<stdio.h>
void digui();
void digui(){
int i=1;
for(i=1;i<=20;i++){
printf("从前有座山,山上有座庙,庙里有两个和尚,老和尚对小和尚说\n");}
}
int main(){
digui();
return 0;
}
欸,可是函数里面这句话也是一直重复的呀。。。
我自己调用自己一直重复打印这句话不就好了吗!
#include<stdio.h>
void digui();
void digui(){
static int i=1;
//静态局部变量,每次都保留上一次的值,但是作用域没变,即递归调用自己时不会初始化为1
printf("从前有座山,山上有座庙,庙里有两个和尚,老和尚对小和尚说\n");
i++;
if(i<=20){
digui();}
}
int main(){
digui();
return 0;
}
静态局部变量(static)
static 用于描述具有文件作用域的变量或函数时,表示将其链接属性从external 修改为 internal,它的作用范围就变成了仅当前源文件可以访问。
但如果将 static 用于描述局部变量,那么效果又会不一样了。默认情况下,局部变量是 auto 的,具有自动存储期的变量。如果使用 static 来声明局部变量,那么就可以将局部变量指定为静态局部变量。static 使得局部变量具有静态存储期,所以它的生存期与全局变量一样,直到程序结束才释放。
递归从原理上来说就是函数调用自身这么一个行为。
来一个错误示范:
#include<stdio.h>
void recursion(void);
void recursion(void){
printf("hi\n");
recursion();//自己调用自己,无限月读!
}
int main()
{
recursion();
return 0;
}
别试!会炸!
于是我们悟道了一个道理:
编写递归程序需要注意的地方
递归程序需要正确设置结束条件,否则递归程序会一直走下去,直到崩溃。
#include<stdio.h>
void recursion(void);
void recursion(void){
static int count=10;
printf("hi\n");
if(--count)
{
recursion();
}
}
int main()
{
recursion();
return 0;
}
栗子:递归求阶乘
n ! = 1 ∗ 2 ∗ 3 ∗ . . . ∗ n n!=1*2*3*...*n n!=1∗2∗3∗...∗n
首先使用循环
使用累乘的思想循环,一次成一个项:
#include<stdio.h>
long fact(int num);
long fact(int num)
{
long result;
for(result=1;num>1;num--)
{
result*=num;
}
return result;
}
int main()
{
int num;
printf("请输入一个正整数:");
scanf("%d",&num);
fact(num);
printf("数%d的阶乘为:%ld",num,fact(num));
return 0;
}
修改为递归
n ! = ( n − 1 ) ! ∗ n n!=(n-1)!*n n!=(n−1)!∗n
( n − 1 ) ! = ( n − 2 ) ! ∗ ( n − 1 ) (n-1)!=(n-2)!*(n-1) (n−1)!=(n−2)!∗(n−1)
… …
2!=1! *2所以若fact()为求阶乘函数,即 n ! = f a c t ( n ) n!=fact(n) n!=fact(n)
则有
n ! = f a c t ( n ) = f a c t ( n − 1 ) ∗ n = f a c t ( n − 2 ) ∗ ( n − 1 ) ∗ n = . . . = f a c t ( 1 ) ∗ 2 ∗ . . . ∗ n n!=fact(n)=fact(n-1)*n=fact(n-2)*(n-1)*n=...=fact(1)*2*...*n n!=fact(n)=fact(n−1)∗n=fact(n−2)∗(n−1)∗n=...=fact(1)∗2∗...∗n
即在fact(n)函数中调用自己计算fact(n-1),而在这继续调用fact(n-2)…一直至fact(1),再逐层将结果返回
#include<stdio.h>
long fact(int num);
long fact(int num)
{
long result;
if(num>0)//结束条件
{
result=num*fact(num-1);//自己调用自己
}
else
{
result=1;
}
return result;
}
int main()
{
int num;
printf("请输入一个正整数:");
scanf("%d",&num);
fact(num);
printf("数%d的阶乘为:%ld",num,fact(num));
return 0;
}
例子:递归求解斐波那契数列
数列长这样子:
1 1 2 3 5 8 13 21 34
规律:
a n = a n − 1 + a n − 2 a_n=a_{n-1}+a_{n-2} an=an−1+an−2
若fun(n)函数为求解 a n a_n an项值,则有
f u n ( n ) = f u n ( n − 1 ) + f u n ( n − 2 ) fun(n)=fun(n-1)+fun(n-2) fun(n)=fun(n−1)+fun(n−2)
即需要调用两次自己!
# include <stdio.h>
int fun(int n);
int fun(int n){
int num;
if(n==1||n==2)
{
num=1;
}
else
num=fun(n-1)+fun(n-2);//调用自己
return num;
}
int main (void)
{
int n;
printf("Enter n:");
scanf("%d",&n);
printf("a_n=%d",fun(n));
return 0;
}
对比一下用循环的思想做:
# include <stdio.h>
int fun(int n);
int fun(int n){
int num1,num2,num;
int i;
num1=num2=1;//纪录前两项为1
if(n==1||n==2)
{
num=1;//纪录前两项为1
}
else
for(i=3;i<=n;i++){
num=num1+num2;//否则为前两项的和
//更新前两项
num1=num2;
num2=num;
}
return num;
}
int main (void)
{
int n;
printf("Enter n:");
scanf("%d",&n);
printf("a_n=%d",fun(n));
return 0;
}
递归的优势和劣势
优势:递归的思考角度跟通常的迭代(你可以理解为 for 循环之类的)迥然不同,所以有时候使用迭代思维解决不了的问题,使用递归思维则一下子迎刃而解。
劣势:递归的执行效率通常比迭代低很多,所以递归程序要更消耗时间;由于递归函数是不断调用函数本身,在最底层的函数开始返回之前,程序都是一致在消耗栈空间的,所以递归程序要“吃”更多的内存空间;递归的结束条件设置非常重要,因为一旦设置错误,就容易导致程序万劫不复(崩溃)。
汉诺塔
在线小游戏:https://www.hannuota.cn/
假设有64个盘子:对于汉诺塔的玩法,可以简单分解为三个步骤:
- 将前 63 个盘子从 X 移动到 Y上,确保大盘在小盘下。
- 将最底下的第 64 个盘子从 X 移动到 Z 上。
- 将 Y 上的 63 个盘子移动到 Z 上。
在游戏中,由于每次只能移动一个圆盘,所以在移动的过程中显然要借助另外一根针才可以实施。也就是说,步骤 1 将 1~63 个盘子移到 Y 上,需要借助 Z;步骤 3 将 Y 针上的 63 个盘子移到 Z 针上,需要借助 X。
所以我们把新的思路聚集为以下两个问题:
- 问题一:如何将 X 上的 63 个盘子借助 Z 移到 Y 上?
- 问题二:如何将 Y 上的 63 个盘子借助 X 移到 Z 上?
解决这两个问题的方法跟解决“如何将 X 上的 64 个盘子借助 Y 移动到 Z 上?”这个问题是一样的,都是可以拆解成 1、2、3 三个步骤来实现。
问题一(“如何将 X 上的 63 个盘子借助 Z 移到 Y 上?”)拆解为:
- 将前 62 个盘子从 X 移动到Z上,确保大盘在小盘下。
- 将最底下的第 63 个盘子移动到 Y 上。
- 将 Z 上的 62 个盘子移动到 Y 上。
问题二(“如何将 Y 上的 63 个盘子借助 X 移到 Z 上?”)拆解为:
- 将前 62 个盘子从 Y 移动到 X 上,确保大盘在小盘下。
- 将最底下的第 63 个盘子移动到 Z 上。
- 将 X 上的 62 个盘子移动到 Y 上。
没错,汉诺塔的拆解过程刚好满足递归算法的定义,因此,对于如此难题,使用递归来解决,问题就变得相当简单了!
#include <stdio.h>
void hanoi(int n, char x, char y, char z);
void hanoi(int n, char x, char y, char z)
{
if (n == 1)
{
printf("%c --> %c\n", x, z); // 剩下底部的那个圆盘
}
else
{
hanoi(n-1, x, z, y); // 将n-1个圆盘从x移动到y
printf("%c --> %c\n", x, z);
hanoi(n-1, y, x, z); // 将n-1个圆盘从y移动到z
}
}
int main(void)
{
int n;
printf("请输入汉诺塔的层数:");
scanf("%d", &n);
hanoi(n, 'X', 'Y', 'Z');
return 0;
}
参考资料
鱼C工作室、论坛