7.5 递归
C通过运行时堆栈支持递归函数的实现。递归函数就是直接或间接调用自身的函数。把计算阶乘和斐波那契s数列用来说明递归,这是非常不幸的。在第一个例子中,递归并没有提供任何优越之处。在第2个例子中,它的效率之低是非常恐怖的。
有趣的是,标准并未说明递归需要堆栈实现。但是,堆栈非常适合于实现递归,所以许多编译器都使用堆栈来实现递归。
程序的目的是把一个整数从二进制形式转换为可打印的字符形式。我们采用的策略是把这个值反复除以10,并打印各个余数。在ASCII码中,字符'7'的值是55,所以需要在余数上加上48来获得正确的字符。但是,使用字符常量而不是整型常量可以提高程序的可移植性。考虑下面的关系:
'0' + 0 = '0'
'0' + 1 = '1'
这些关系要求数字在字符集中必须连续。所有常用的字符集都符号这个要求。
很容易看出在余数上加上'0'就可以产生对应字符的代码。接着就打印出余数。下一步是取得商,4267/10等于426。然后用这个值重复上述步骤。
这种处理方法存在的唯一问题就是它产生的数字次序恰好相反,它们是逆向打印的。
这个程序的递归实现了某种类型的螺旋状while循环。while循环在循环体每次执行时必须取得某种进展,逐步迫近循环终止条件。递归函数也是如此,它在每次递归调用后必须越来越接近某种限制条件。当递归函数符合这个限制条件时,它便不再调用自身。
在程序7.6中,递归函数的限制条件就是变量quotient为零。在每次递归调用之前,我们都把quotient除以10,所以每递归调用一次,它的值就越来越接近零。当它最终变成零时,递归便告终止。
/*
**接受一个整型值(无符号),把它转换为字符并打印它。前导零被删除。
*/
#include <stdio.h>
void binary_to_ascii( unsigned int value ){
unsigned int quotient;
quotient = value / 10;
if(quotient != 0 ){
binary_to_ascii( quotient );
}
putchar( value % 10 + '0' );
}
程序7.6 将二进制整数转换为字符 btoa.c
这个函数的工作流程:
1.将参数值除以10。
2.如果quotient的值为非零,调用binary_to_ascii打印quotient当前值的各位数字。
3.接着,打印步骤1中除法运算的余数。
在第2个步骤中,我们需要打印的是quotient当前值的各位数字。我们所面临的问题和最初的问题完全相同,只是变量quotient的值变小了。因此递归会终止。
一旦理解了递归,阅读递归函数最容易的方式不是纠缠于它的执行过程,而是相信递归函数会顺利完成它的任务。如果每个步骤正确无误,限制条件设置正确,并且每次调用之后更接近限制条件,递归函数总是能够正确地完成任务。
7.5.1 追踪递归函数
为了能理解递归的工作原理,需要追踪递归调用的执行过程。追踪一个递归函数执行过程的关键是理解函数中所声明的变量是如何存储的。当函数被调用时,它的变量的空间是创建于运行时堆栈上的。以前调用的函数的变量仍保留在堆栈上,但它们被新函数的变量所掩盖,因此是不能访问的。
当递归函数调用自身时,情况也是如此。每进行一次新的调用,都将创建一批变量,它们将掩盖递归函数前一次调用所创建的变量。在追踪一个递归函数的执行过程中,必须把分属不同次调用的变量区分开来,以避免混淆。
程序7.6的函数有两个变量:参数value和局部变量quotient。下面显示了堆栈的状态,当前可以访问的变量位于栈顶。所有其它调用的变量饰以灰色阴影,表示它们不能被当前正在执行的函数访问。
value 4267 quotient
其他函数调用使用的变量
执行除法运算之后
value 4267 quotient 426
其他函数调用使用的变量
函数第二次被调用之初,堆栈的内容如下:
value 426 quotient
value 4267 quotient 426
其他函数调用使用的变量
再次执行除法运算之后,堆栈的内容如下:
value 426 quotient 42
value 4267 quotient 426
其他函数调用使用的变量
...
不算递归调用语句本身,到目前为止所执行的语句只是除法运算以及对quotient的值进行测试。由于递归调用使这些语句重复执行,因此它的效果类似循环:当quotient的值非零时,把它的值作为初始值重新开始循环。但是,递归调用将会保存一些信息(这一点与循环不同),也就是保存在堆栈中的变量值。这些信息很快就会变得非常重要。
现在quotient的值变成了零,递归函数便不再调用自身,而是开始打印输出。然后函数返回并开始销毁堆栈上的变量值。
value 4 quotient 0 输出:4
value 42 quotient 4
value 426 quotient 42
value 4267 quotient 426
其他函数调用使用的变量
接着函数返回,它的变量从堆栈中销毁。接着,递归函数的前一次调用重新继续执行,它所使用的变量的是自己的变量,它们现在位于堆栈的顶部。因为它的value值是42,所以调用putchar后打印出来的数字是2。
value 42 quotient 4 输出:42
value 426 quotient 42
value 4267 quotient 426
其他函数调用使用的变量
...
然后,这个递归函数就彻底返回到其他函数调用它的地点。
7.5.2 递归与迭代
递归是一种强有力的技巧,和其他技巧一样,它也可能被误用。阶乘的定义往往就是以递归的形式的形式描述的,如下所示:
factorial(n) = { n <= 0: 1
n > 0: n * factorial(n - 1)
}
这个定义同时具备了递归所需要的两个特性:存在限制条件,当符合这个条件时递归便不再继续;每次递归调用之后越来越接近这个限制条件。
这个函数能够产生正确的结果,但它并不是递归的良好方法。为什么?递归函数调用将涉及一些运行时开销---参数必须压到堆栈中、为局部变量分配内存空间(所有递归均如此,并非特指这个例子)、寄存器的值必须保存等。当递归函数的每次调用返回时,上述这些操作必须还原,恢复成原来的样子。所以,基于这些开销,对于这个程序而言,它并没有简化问题的解决方案。
/*
**用递归方法计算n的阶乘。
*/
long factorial( int n ){
if( n <= 0 ){
return 1;
}else{
return n * factorial( n - 1 );
}
}
程序7.7a 递归计算阶乘 fact_rec.c
程序7.7b使用循环计算相同的结果。尽管使用简单循环的程序不甚符合前面阶乘的数学定义,但它却能更为有效地计算出相同的结果。如果仔细观察递归函数,就会发现递归调用是函数所执行的最后一项任务。这个函数是尾部递归(tail recursion)的一个例子。由于函数在递归调用返回之后不再执行任何任务,因此尾部递归可以很方便地转换成一个简单循环,完成相同的任务。
/*
**用迭代方法计算n的阶乘。
*/
long factorial( int n ){
int result = 1;
while( n > 1 ){
result *= n;
n -= 1;
}
return result;
}
程序7.7b 迭代计算阶乘 fact_itr.c
提示:
许多问题是以递归形式进行解释的,这只是因为它比非递归形式更为清晰。但是,这些问题的迭代形式往往比递归实现效率更高,虽然代码的可读性可能稍差一些。当一个问题相当复杂,难以用迭代形式实现时,此时递归实现的间接性便可以弥补它所带来的运行时开销。
这里有一个更为极端的例子,斐波那契数就是一个数列,数列中每个数的值就是它前面两个数的和。这种关系常常用递归的形式进行描述:
Fibonacci(n) = {
n <= 1: 1
n = 2: 1
n > 2: Fibonacci(n-1) + Fibonacci(n-2)
}
同样,这种递归形式的定义容易诱导人们使用递归来解决问题。这里有一个陷阱:它使用递归步骤计算Fibonacci(n-1)和Fibonacci(n-2)。但是,在计算Fibonacci(n-1)时也将计算Fibonacci(n-2)。这个额外的计算代价有多大呢?
答案是它的代价远远不止一个冗余计算---每个递归调用都触发另外两个递归调用,而这两个调用的任何一个还将触发两个递归调用,再接下去的调用也是如此。如果Fibnacci的值被计算了超过一次,除了其中之一外,其余的纯属浪费。这个额外的开销真是相当恐怖!
/*
**用递归方法计算n个斐波那契数的值。
*/
long fibonacci( int n ){
if( n <= 2 ){
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
程序7.8a 用递归计算斐波那契数 fib_rec.c
程序7.8b使用一个简单循环来代替递归。它的效率提高了几十万倍!
在使用递归方式实现一个函数之前,先问问自己使用递归带来的好处是否抵得上它的代价。而且必须小心:这个代价可能比看上去要大得多。
/*
**用迭代方法计算第n个斐波那契数的值。
*/
long fibonacci( int n ){
long result;
long previous_result;
long next_older_result;
result = previous_result = 1;
while( n > 2 ){
n -= 1;
next_older_result = previous_result;
previous_result = result;
result = previous_result + next_older_result;
}
return result;
}
程序7.8b 用迭代法计算斐波那契数 fib.iter.c