递归的实质
我们知道:函数可以调用其他函数实现部分功能,那么,函数可不可以调用自己呢? 答案:当然!
故事
从前有座山,山上有座庙,庙里有两个和尚,老和尚对小和尚说:从前有座山,山上有座庙,庙里有两个和尚,老和尚对小和尚说:从前有座山,山上有座庙,庙里有两个和尚,老和尚对小和尚说:……
怎么用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;
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;
if(n==1||n==2)
{
num=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);
printf("%c --> %c\n", x, z);
hanoi(n-1, y, x, z);
}
}
int main(void)
{
int n;
printf("请输入汉诺塔的层数:");
scanf("%d", &n);
hanoi(n, 'X', 'Y', 'Z');
return 0;
}
参考资料
鱼C工作室、论坛
|