您的位置:首页 > 理论基础 > 数据结构算法

数据结构与算法——递归简论

2016-05-08 11:04 274 查看
本文简述了基于C语言的递归(recursive)和使用递归的四条基本法则

我们先用数学语言来描述一下什么是递归,如:

F(X)= F(X) =

{0,2F(X−1)+x2, X=0 X≠0\begin{cases}
0, & \text{ $X=0$} \\
2F(X-1)+x^2, & \text{ $X\neq0$}
\end{cases}

当一个函数使用它自己来定义时就称为是递归。在C中,函数F(X)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∗42F(3)+4*4,紧接着调用
F(3)
……此时,F(0)F(0)必须被赋值,否则,程序将会不断地执行下去,直至崩溃。我们把F(0)=0F(0)=0的情况叫做基准情形(base case)。我们再来看一个错误使用递归的例子:

int Bad( unsigned int N){
if (N == 0)
return 0;
else return Bad(N/3+1)+N-1;
}


我们可以看到,除00之外,对于任意的NN,程序都不可能算出结果。因此,我们可以得出递归的两个基本准则:

基准情形。设计递归时总要有某些基准的情形,它们不用递归就可以求解,如上面的N==0N==0的情况。

不断推进(making progress)。既然有了基准情形,那么对于那些需要递归求解的情形,递归调用必须总能够朝着基准情形的方向推进。

下面我们再来看看使用递归打印一个正整数NN的例子(假设现在的I/O只能处理单个数字并将其输出到终端,
Printout(N)
为处理单个数字的输出函数),例如,
PrintDigit(4)
就是将“4”输出到终端。现在,我们需要实现将“12345”输出到终端,首先需要打印出‘1’,然后是‘2’……假设我们已经打印出了”1234”,再打印’5’时,使用语句
PrintDigit(N%10)
就可以完成。对于前面的情况,我们可以用同样的方法解决。因此,我们可以使用语句
PrintOut(N/10)
递归地解决这个问题。

? - 也许这里会产生疑问,“上面所说的基准情形如何定义?”。如果0≤N<100 \le N \lt 10,我们就使用
PrintDigit(N)
直接输出NN,所以
PrintDigit(N)
就是基准情形。而对于一个正整数NN,我们可以通过
PrintOut(N)
来用较小的正整数定义它,这样也保证了递归的不断推进。

过程代码如下:

void PrintOut( unsigned int N ){
if(N >= 10)
printOut( N/10 );
PrintDigit( N%10 );
}


证明(前方高能)

首先,如果N只有一位数字,那么程序显然是正确的,因为它只需调用一次
PrintDigit(N)


然后,设
PrintOut(N)
对所有kk位或者位数更少的数都有效。对于k+1k+1位的数,可以通过前kk位数字和最后一位数字来表示。前kk位数字就是程序中的
(int)(N/10)
,即N/10N/10后向下取整,而最后一位数字是Nmod10N mod 10(
N%10
)。因此,该程序能够正确地打印出任意k+1k+1位数。于是,根据归纳法,所有的数都能被正确地打印出来。

因此,我们可以得出递归的第三条法则:

3. 设计法则(design rule)。假设所有的递归调用都能运行(这也是上面为什么要证明的原因)。当设计递归程序时一般没有必要知道簿记管理的细节,因为有时追踪实际的递归调用序列是非常困难的。另一方面,这也体现了递归的好处——计算机能够算出复杂的细节。

递归的主要问题是隐含的簿记开销,虽然这些开销几乎总是合理的(既简化了算法设计,又给出了更加简介的代码),但要注意的是,不要尝试用递归来代替简单的for循环。

最后,递归的第四条法则是:

4. 合成效益法则(compound interest rule)。在求解一个问题的同一实例时,切勿在不同的递归调用中做重复性的工作。

Reference:

[1]. Data Structures and Algorithm Analysis in C Second Edition, Mark Allen Weiss
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: