一、 理解概念
C语言允许一个函数调用自身,这种过程被称为递归(Recursion)。程序使用递归处理特殊的问题,如阶乘、 Ackermann函数,反序等等。实际上,如果不考虑运行时内存的开消,任何使用赋值语句、if-else和while结构的函 数都可以用递归方式重写。
“递归”可以跟据字面去理解,“递”就是一级一级递进,“递”可以想象成通过一段台阶走下地下室,“归”则是归来 的意思,就像通个了“递”这些阶级到了地下室之后,又延着所走的台阶返回原点。
你还可以将“递归”想象成“我肚里面有一个我”,如果我肚里有一个我,那么肚里的我肚里还有一个我,这样一路我 下去,我我我我,将会无穷无尽,因而递归需要一个终止的条件,就假设世界上只能同时存在5个我,这样,“我肚里面 有我”就一共只有五个肚,五个我,最后第五个我的肚里不能再有我了。
“我肚里有我”,这就相当函数里面有本函数,函数处理事务,就相当于我吃饭, 当我吃饭时,我里面的我也要吃 饭,但所有的我入口都是最外面的一张嘴,于是,我吃饭,饭到了肚子里被肚子里的我吃了我下去的饭,肚子里的我的 肚子里的我再把他的饭吃了,这样的情况,只有把最里面的我的肚子喂饱了,外面的我才可以吃到饭。程序的函数也是 一样,它必须要等里面的函数处理完了问题,外面的函数才可以着手处理问题。就好比去地下室拿东西,你必须走完所 有阶梯才到达地下室,然后才可以返回。
总之,我们记着,只要是递归,就必须要等里面的我(函数)完成了任务,才轮到外面一级的我(函数)做任务。
二、 递归的基本原理
1. 每一级的函数调用都有自已的变量。
2. 每一次函数调用都会有一次返回。
3. 递归函数中,位于递归调用前的语句和各级被调函数具有相同的执行顺序。
4. 递归函数中,位于递归调用后的语句和各级被调函数的顺序相反。
5.虽然每一级都有自己的变量,但是函数代码并不会复制。函数代码是一系列的计算机指令,而函数调用就是从头执行这个指令集的一条命令。
6.递归函数中必须包含可以终止递归的语句。
三、原理验证
#include <stdio.h>
void up_and_down(int);
int main(void)
{
up_and_down(1);
return 0;
}
void up_and_down(int n)
{
printf("Level %d: n location %p\n", n, &n);
if(n < 3)
up_and_down(n+1);
printf("Level %d: n location %p\n", n, &n);
}
编译后执行的结果:
Level 1: n location 0x7fffffffe30c 从level1、level2、level3中可以看出n有三个不同的地址,即是说经过三次递归调用,各有不同的n变量。
Level 2: n location 0x7fffffffe2ec 下面的3级level,可以看出3次递归调用返回了三次,验证了每次调用都有返回。
Level 3: n location 0x7fffffffe2cc 第一和第二个printf用来验证递归调用前的函数执行顺序,验证了它和递归调用有相同的执行顺序。
Level 3: n location 0x7fffffffe2cc 第三和第四个printf则说明了递归后的函数是以反序方式来执行的
Level 2: n location 0x7fffffffe2ec
Level 1: n location 0x7fffffffe30c
我们再用GDB跟踪程序:
先用gdb 加载程序,键入start运行后停在main入口处
Temporary breakpoint 2, main () at recur.c:6
6 up_and_down(1);
stepi跟踪程序进入调用函数
(gdb) stepi
0x0000000000400589 6 up_and_down(1);
用backtrace查看内存运行栈,看出函数已压入栈中
(gdb) bt
#0 up_and_down (n=0) at recur.c:11
#1 0x000000000040058e in main () at recur.c:6
往下执行到n=3时,栈中已压入三个函数在main函数之上,从中可以看出,每一次函数调用,就需要向栈中压入数据,因为每次调用都要压栈,所以每次调用的函数的变量都是独立的,又所以每一次调用都会有返回。
(gdb) bt
#0 up_and_down (n=3) at recur.c:13
#1 0x00000000004005d7 in up_and_down (n=2) at recur.c:15
#2 0x00000000004005d7 in up_and_down (n=1) at recur.c:15
#3 0x000000000040058e in main () at recur.c:6
因为栈的特性,后进入栈的函数获得先返回的机会,所以位于递归调用后的语句和各级被调函数的顺序相
反,当n>时函数开始返回,首先返回的是n=3时的函数。
跟上面比较, up_and_down(n=3)的函数已经返回
(gdb) bt
#0 up_and_down (n=2) at recur.c:17
#1 0x00000000004005d7 in up_and_down (n=1) at recur.c:15
#2 0x000000000040058e in main () at recur.c:6
往下执行直到main返回,程序结束。
(gdb) c
Continuing.
Level 2: n location 0x7fffffffe2ec
Level 1: n location 0x7fffffffe30c
[Inferior 1 (process 6893) exited normally]
用GDB调试可以看出,递归如果层数太多,超出了栈的容量就会造成程序死机,这就是递归的缺点;同时,函数调用每次都要压栈处理,递归层数过多就会影响程序的运行速度。
四、递归处理反序
返回值需要反序排列时,递归能有效地精简代码
例如:十进制转换成二进制,用除二取余,倒序排列
意思是:将一个十进制数除以二,得到的商再除以二,依此类推直到商等于一或零时为止,倒取将除得的余数,即换算为二进制数的结果。 例如把52换算成二进制数,计算结果如图:
52除以2得到的余数依次为:0、0、1、0、1、1,倒序排列,所以52对应的二进制数就是110100。
用C语言可以这样编码:
#include <stdio.h>
void to_binary(int);
int main(void)
{
int number;
printf("Enter a number or 'q' to exit:\n");
while (scanf("%d", &number) == 1)
{
printf("Binary equivalent:");
to_binary(number);
putchar('\n');
printf("Enter a number or 'q' to exit:\n");
}
printf("Done!\n");
return 0;
}
void to_binary(int n) {
int r;
r = n%2;
if (n >= 2)
to_binary(n/2);
putchar('0' + r);
return;
}
上面用递归的方法比用循环的方法要简单很多,你可以用循环的方法重编上面的函数去对比。