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

  • 本文简述了基于C语言的递归(recursive)和使用递归的四条基本法则

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

{0,2F(X1)+x2, X=0 X0

当一个函数使用它自己来定义时就称为是 递归。在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)+44 ,紧接着调用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 ,程序都不可能算出结果。因此,我们可以得出递归的两个基本准则:

  1. 基准情形。设计递归时总要有某些基准的情形,它们不用递归就可以求解,如上面的 N==0 的情况。
  2. 不断推进(making progress)。既然有了基准情形,那么对于那些需要递归求解的情形,递归调用必须总能够朝着基准情形的方向推进。

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

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

过程代码如下:

void PrintOut( unsigned int N ){
    if(N >= 10)
        printOut( N/10 );
    PrintDigit( N%10 );
}
  • 证明(前方高能)

首先,如果N只有一位数字,那么程序显然是正确的,因为它只需调用一次PrintDigit(N)
然后,设PrintOut(N)对所有 k 位或者位数更少的数都有效。对于 k+1 位的数,可以通过前 k 位数字和最后一位数字来表示。前 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

你可能感兴趣的:(数据结构与算法分析)