本文基于C语言来理解,其他语言可做借鉴。
-1.C语言在内存中的组织形式
理解递归首先要理解C语言在内存中的组织形式。基本上,一个可执行程序由四个区域组成:代码段,静态数据区,栈和堆。如下图的左边的图:
每一个区域的具体内容在上图中给出。
当C程序调用一个函数时,栈中会分配一块空间来保存于这个栈调用相关的信息。每一个调用都被当做是活跃的。栈上的那块存储空间称之为活跃记录,或者栈帧。
一份活跃记录由上面图中的五个区域组成,其中
输入参数:传递到活跃记录中的参数,即该活跃记录对应的函数的输入参数;
输出参数:传递给在活跃记录中调用的函数所使用的参数;
输出参数不同于函数的返回值,这里的输出参数是该活跃记录产生的一个输出,再调用下一个函数时,成为了下一个活跃记录的输入参数。这里可能有点绕,但是待会由例子可以更好的理解。
要知道的是: 函数调用的活跃记录将一直存在于栈中,直到这个函数调用结束。
-2.递归
什么叫做递归?
递归就是自己调用自己(直接或者间接)。——这可以说是递归函数的最直观的定义。
递归函数是一种可以调用自身的函数,每次成功的调用都使得输入变得更加精细,使我们越来越接近问题的答案。
2.1 基本递归
以阶乘作为例子来说明,阶乘的定义如下:
由此,我们很容易得到C代码:
int factorial(int n) { if (n < 0) { /* printf("Wrong Input!!!\n") ;*/ return 0 ; } else if (n == 0 || n == 1) { return 1 ; } else return n*factorial(n-1) ; }
调用上面的函数计算4!,其执行的顺序可以由下图形象的给出:
由图我们可以知道递归过程的两个基本阶段:递推与回归。在递推阶段进行的是函数的一次次调用,直到终止条件;在回归阶段进行计算。
这种递归成为基本递归方式,也称为线性递归,这种递归方式在执行时,在内存中开辟的栈如下:
栈先进后出的特点整好满足了函数调用和返回的顺序。
基本递归有很多缺点:多次递归调用时,占用空间大;大量信息保存和恢复,生成和销毁活跃记录耗时;冗余计算严重(这一点会在下一篇给出,有兴趣也可以自己查阅资料)。
2.2 尾递归
如果一个函数中所有递归形式的调用,都出现在函数的末尾即为尾递归。
当递归调用是整个函数体最后执行的语句,且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归的特点是在回归过程中不需要做任何操作。
e.g. 求阶乘
C语言实现为:
int fact_tail(int n,int a) { if (n < 0) { return 0 ; } else if (n == 0 || n == 1) { return a ; } else return fact_tail(n-1,n*a) ; }
调用这个函数计算4!,其执行的顺序可以由下图形象的给出:
由此可见,在回归过程中,没有任何操作,所有操作都在递推阶段完成。
这种递归在内存中开辟的栈为:
在上图尾递归的调用每一个当前活跃记录都覆盖了上个当前活跃记录,所以自始至终只有一个活跃记录,并非上图中的四个,上图只是为了直观理解。
至此,我们再来理解尾递归和基本递归。
在基本递归函数factorial()中,最后一次调用时是将上一次调用factorial(3)产生的值(6)与n=4相乘,由此我们可以看到factorial()的返回值是作为表达式(乘法)的一部分的,所以这不是尾递归;
而在尾递归函数fact_tail()中,对fact_tail()的单次调用是函数返回前执行的最后一条语句(返回24前,调用fact_tail(1,24),得到了24),因此这是一个尾递归函数。
参考:《算法精解:C语言描述》