- 本文简述了基于C语言的递归(recursive)和使用递归的四条基本法则
我们先用数学语言来描述一下什么是递归,如:
F(X)=
当一个函数使用它自己来定义时就称为是 递归。在C中,函数 F(X) 的实现如下:
int F( int X ){
if( X == 0 )
return 0;
else return 2*F(X-1)+X*X;
}
实际上,递归调用在处理上与其他的调用没什么不同。如果以F(4)
调用函数F(int x)
,那么程序就会计算
2F(3)+4∗4
,紧接着调用F(3)
……此时,
F(0)
必须被赋值,否则,程序将会不断地执行下去,直至崩溃。我们把
F(0)=0
的情况叫做基准情形(base case)。我们再来看一个错误使用递归的例子:
int Bad( unsigned int N){
if (N == 0)
return 0;
else return Bad(N/3+1)+N-1;
}
我们可以看到,除
0
之外,对于任意的
- 基准情形。设计递归时总要有某些基准的情形,它们不用递归就可以求解,如上面的 N==0 的情况。
- 不断推进(making progress)。既然有了基准情形,那么对于那些需要递归求解的情形,递归调用必须总能够朝着基准情形的方向推进。
下面我们再来看看使用递归打印一个正整数
N
的例子(假设现在的I/O只能处理单个数字并将其输出到终端,Printout(N)
为处理单个数字的输出函数),例如,PrintDigit(4)
就是将“4”输出到终端。现在,我们需要实现将“12345”输出到终端,首先需要打印出‘1’,然后是‘2’……假设我们已经打印出了”1234”,再打印’5’时,使用语句PrintDigit(N%10)
就可以完成。对于前面的情况,我们可以用同样的方法解决。因此,我们可以使用语句PrintOut(N/10)
递归地解决这个问题。
? - 也许这里会产生疑问,“上面所说的基准情形如何定义?”。如果PrintDigit(N)
直接输出
N
,所以PrintDigit(N)
就是基准情形。而对于一个正整数PrintOut(N)
来用较小的正整数定义它,这样也保证了递归的不断推进。
过程代码如下:
void PrintOut( unsigned int N ){
if(N >= 10)
printOut( N/10 );
PrintDigit( N%10 );
}
- 证明(前方高能)
首先,如果N只有一位数字,那么程序显然是正确的,因为它只需调用一次PrintDigit(N)
。
然后,设PrintOut(N)
对所有
k
位或者位数更少的数都有效。对于(int)(N/10)
,即
N/10
后向下取整,而最后一位数字是
Nmod10
(N%10
)。因此,该程序能够正确地打印出任意
k+1
位数。于是,根据归纳法,所有的数都能被正确地打印出来。
因此,我们可以得出递归的第三条法则:
3. 设计法则(design rule)。假设所有的递归调用都能运行(这也是上面为什么要证明的原因)。当设计递归程序时一般没有必要知道簿记管理的细节,因为有时追踪实际的递归调用序列是非常困难的。另一方面,这也体现了递归的好处——计算机能够算出复杂的细节。
递归的主要问题是隐含的簿记开销,虽然这些开销几乎总是合理的(既简化了算法设计,又给出了更加简介的代码),但要注意的是,不要尝试用递归来代替简单的for循环。
最后,递归的第四条法则是:
4. 合成效益法则(compound interest rule)。在求解一个问题的同一实例时,切勿在不同的递归调用中做重复性的工作。
Reference:
[1]. Data Structures and Algorithm Analysis in C Second Edition, Mark Allen Weiss