c允许函数自己调用自己,这种调用过程称为递归(recursion)。适当的使用递归函数可以让代码更简洁,但效率较低。
演示递归
首先,我们通过演示了解一下递归是如何工作的。
请看下例
void up_and_down(int n)
{
printf("leve %d :n location %p\n", n, &n);
if (n < 3)
up_and_down(n + 1);
printf("Leve %d :n location %p\n", n, &n);
}
void main()
{
up_and_down(1);
}
下面是我们在系统中得输出
leve 1 :n location 0118FD60
leve 2 :n location 0118FC88
leve 3 :n location 0118FBB0
Leve 3 :n location 0118FBB0
Leve 2 :n location 0118FC88
Leve 1 :n location 0118FD60
分析:
- main函数调用第一层up_and_down,此时参数n的值是1;
- 第一层up_and_down中,由于n<3,调用第二层up_and_down,参数为n+1,那么在第二层中实际参数n的值应为2;
- 第二层up_and_down中,同样由于n<3,调用第三层up_and_down,参数为n+1,那么在第二层中实际参数n的值应为3;
- 当执行到第三层时,n的值为3,if条件为假,up_and_down不在调用自己,执行下一条语句,打印Leve 3;
- 第三层调用结束,控制返回第二层。因为在第二层中最后执行的语句为第三层调用,在控制返回之后,接着执行第三层调用的下一条语句,打印Leve2;
- 第二层调用结束,控制返回第一层。与上同理,打印Leve3;
- 第一层调用结束,控制返回main函数。
- 注意,每级递归的变量n都属于本级私有,我们可以用程序输出的地址值验证。leve1与Leve1的地址相同,leve2与Leve2的地址相同,leve3与Leve3的地址同样相同;
从上例中我们可以窥探递归的基本原理。简单来说,就是函数先向下一级一级调用自己,在结束后再向上一级一级返回,并且每一次调用都有自己的变量。同时,我们也能引出使用递归的两个基本条件。
第一个条件:递归函数必须包含能使递归结束的语句。在上例中,就是if(n<3)。如果没有这个语句,递归就会无限进行下去。
第二个条件:每进行一次递归,参数都要更接近递归结束的条件。在上例中,递归结束条件为“n<3”,没进行一次递归,参数变为n+1,即更接近“n<3”。
举例应用
首先,我们必须清楚函数递归的基本解题思路为大事化小。接下来我们将在举例中细细品味这一思路。(谨记,在使用递归时,我们一定要记住我们正在写的函数有什么作用)
首先让我们尝试使用递归逐位打印一个正整数。
例如我要逐位打印123。
输出结果应为1 2 3 。
假如我写了一个print_tf()函数可以递归逐位打印一个正整数,那么逐位打印123就可以写成prin_tf(123)。
我们细看,这个prin_tf(123),是不是可以拆分成prin_tf(12)和printf("3")。
再进一步,就可以拆分成prin_tf(1)和printf("2")和printf("3")。分析到这里相信一些小伙伴已经有想法了,请看笔者的答案。
#include<stdio.h>
void prin_tf(int n)
{
if (n > 9) //结束条件
prin_tf(n / 10); //递归调用
printf("%d ", n%10); //打印
}
void main()
{
int n;
scanf("%d", &n);
prin_tf(n);
}
在上例中,我们使用n/10和n%10来分别达成拆分整数和逐位打印的功能。
- 当n>9时,n仍为两位以上整数,我们继续调用prin_tf(n/10),用n/10拆掉此时n的个位数,直到n<9,即n本身是一个个位数,不可继续拆。
- 在调用结束,逐层返回时再利用printf("%d",n%10)逐步输出在递归调用时被拆掉的个位数。此题得解。
斐波那契数列
斐波那契数列的定义如下:第一个数字和第二个数字都是1,而后续的每个数字都是其前两个数字之和。例如,该数列的前几个数字为:1、1、2、3、5、8、13。下面我们要创建一个函数,接受正整数n,输出相应的斐波那契数列值。
首先,把函数命名为Fib(),它能接受正整数n,返回相应的斐波那契数列值。那么根据斐波那契数列的定义,我们能得出Fib(n)等于Fib(n-1)+Fib(n-2)。特别的,当n为1或2时,Fib(n)等于1。
int Fib(int n)
{
if (n > 2)
return Fib(n - 1) + Fib(n - 2);
else
return 1;
}
使用递归计算斐波那契数列,不论思路还是成型的代码都非常的简单,但它也有一个巨大缺陷。该函数每一级递归都调用本身两次,都会创建两个新变量,即每一次递归创建的变量都是上一级创建变量的两倍,当第n次递归时创建的变量数为2^(n-1)个!在本例中,按指数增长的变量数量很快就会消耗掉计算机的大量内存,很可能导致程序崩溃。
所以在程序中使用递归要特别注意,尤其是效率优先的程序。
青蛙跳台问题
现在让我们通过青蛙跳台的问题进一步体会函数递归大事化小的奇妙思路。
问题如下:现有一只青蛙要跳台阶,它可以一次跳一层台阶,也可以一次跳两层台阶。问当它跳n层台阶时有几种跳法。
面对这个问题,我们不妨像处理第一的例题一样,从后向前拆分。
首先我们给一个函数frog(),定义它能求出n层台阶对应的跳法。
当n层台阶时,若我们仅考虑最后一跳的跳法就只有两种选择,最后一跳跳一层和跳两层。
当最后一跳跳一层时,前面n-1层就有frog(n-1)种跳法。
当最后一跳跳两层时,前面n-2层就有frog(n-2)种跳法。
那么第n层就有frog(n-1)+frog(n-2)种跳法。
我们突然发现这和斐波那契数列在求法上是相同的,只是在本题中,n的值为2时,应返回2。
int frog(int n)
{
if (n == 1)
return 1;
else if (n == 2)
return 2;
else
return frog(n - 1) + frog(n - 2);
}
汉诺塔问题
最后,让我们尝试解决一个经典递归问题,汉诺塔问题。
问题如下:设有三个塔座A,S,C,在A塔上有n个盘片,盘片大小不等,按大盘在下,小盘在上的顺序叠放着。现要借助于B塔,将这些盘片移动到C塔上,且移动中仍保持大盘在下,小盘在上的规律,每次只能移动一个盘片。
首先,我们仍是先定义一个hanoi(int n,char a,char b,char c)可以实现上述操作。
为了明确具体思路,我们可以先将问题简化为两个盘片的移动问题。这时,我们只需先将A塔上的小盘片移动到B塔上,再将A塔上的大盘片移动到C塔上,最后将B塔上的小盘片移动到C塔上就可以了。
照猫画虎,我们就可以得到n个盘片的移动问题的大体解题思路。首先,我们需将A塔上的(n-1)个盘片移动到B塔上,再将A塔上最大的盘片移动到C塔上,最后将B塔上的(n-1)个盘片移动到C塔上就可以了。
具体该怎么去移动(n-1)个盘片呢,利用我们定义的hanoi()函数。若hanoi(n,'A','B','C')是将n个盘片从A塔移动到C塔,那么hanoi(n-1,'A','C','B')就是将n-1个盘片从A塔移动到B塔。同理,我们可以利用hanoi(n-1,'B','A','C'),将n-1个盘片从B塔移动到C塔。hanoi()是怎么实现的呢,我们不正在写吗?(笑)
到此我们就得到了递归主体的写法。
void hanoi(int n,char a,char b,char c) //a,b,c是接受来自main函数的'A','B','C'的参数变量
{
hanoi(n - 1, a, c, b);
printf("%c->%c\n", a, c);
hanoi(n - 1, b, a, c);
}
但是我们的递归还缺少一个停止条件。
在hanoi调用自身时,我们设的参数是n-1,即每递归一层参数的值较上一层小1,直到某一层的层数为1。当这一层的参数为1时,A塔上只有一个盘片,我们直接将其移到C塔上即可。所以停止条件我们设为n等于1。
那我们补全代码
#include<stdio.h>
void hanoi(int n,char a,char b,char c)
{
if (n == 1) //结束条件
printf("%c->%c\n", a, c);
else
{
hanoi(n - 1, a, c, b);
printf("%c->%c\n", a, c);
hanoi(n - 1, b, a, c); //递归主体
}
}
void main()
{
int n;
scanf("%d", &n);
hanoi(n, 'A', 'B', 'C');
}
试运行,输入3:
A->C
A->B
C->B
A->C
B->A
B->C
A->C
到此问题得解。
文章到此结束,谢谢观看。