递归是程序设计中常用到的一种简单易懂的方法,在很多场合下,利用递归可以大量减少代码量。
递归往往能体现设计者头脑的聪慧,简单的递归函数省去了大段大段的代码,让人叹服不已。那么,递归的设计又有怎样的固定思路呢?本文将介绍递归与数学归纳法之间的联系,希望给读者一些启迪。
数学归纳法是数学中重要的一种证明方法。当证明一个数学定理时,采用数学归纳法的思路是,先证明对于简单的可以代入的数,定理成立;再在假设定理对某一数N成立的前提下,证明N+1也是定理成立。其实,数学归纳法利用的是递推的原理,形象地可以叫做多米诺原理。因为N+1的成立就可以向前向后递推所有数都成立。
而递归利用的也是递推的原理,在整个程序中,反复实现的将是同一个原理。在这一点上,递归与数学归纳法本质是相同的。所以往往可以利用数学归纳法来设计递归的实现。计算机是数学应用的一个分支在这里体现的淋漓尽致。
这里我们先来看一个例子,非常简单,设计一程序,求自然数N的阶乘N!:
现在已知N!=N*(N-1)*(N-2)*(N-3)*…*2*1
首先可知当N=1时 N!=1
第二步可设当R(N)=N!,R(N+1)=(N+1)!
第三步,求R(N+1)与R(N)之间的关系。R(N)=N!,
而R(N+1)=(N+1)!=(N+1)*(N)*(N-1)*…*2*1=(N+1)*N!=(N+1)*R(N)
即:R(N+1)=(N+1)*R(N) =〉 R(N)=N*R(N-1)
现在根据这个公式草略地构造一个函数:
factorial (int N)
{
return N * factorial (N – 1) /* 递归部分 */
}
接下来补充截止部分,这一部分在整个过程中只使用一次,没有它,程序就将无限递归下去。可以说它是程序运行栈的栈顶,到了它,就开始一步步退栈了。
函数改为:
factorial (int N)
{
if (N == 1) return 1;
return N * factorial (N – 1) /* 递归部分 */
}
上面的步骤是可以颠倒的,而且首先设计截至部分还要好一些。
现在来总结设计递归程序的步骤:
一、用数学归纳法分析问题,根据数学归纳法的第一步得出截至部分。
二、根据数学归纳法的第三步来构造函数的递归部分。其实这个求解过程就是找出R(N)与R(N-1)的关系式。
现在利用总结出的方法做一个练习,比较经典的汉诺塔。
汉诺塔想必大家都知道:三个立柱(命名为from、temp、to,from为圆盘初始所在立柱、to是目标立柱),N个直径不相等的圆盘,将圆盘从from上一个一个移动在to上,要求,每次只能移动一个圆盘,而且只能在三个立柱之间移动。不能出现大盘压小盘的情况。
首先用数学归纳法分析:
当只有一个圆盘的时候,我们可以确定唯一动作:直接将圆盘从from移动到to上。
现在假设有N个圆盘在from上,而我们可以将这些圆盘最终按要求移动到to上(当然也可以移动到temp上)。
那么我们可以证明如果有N+1个时候,我们也可以将圆盘全部按要求移动到to上:因为我们可以先将上面的N个移动到temp上(第二步已假设),再把剩下的最后一个移动到to上,再把temp上的移动到to上。
按照我们总结过的递归函数设计步骤来设计程序:
首先,确定截至部分:当只有一个圆盘移动的时候,直接将它移动到to上。即:if (n == 1) move (n, from, to);
(这里的move函数意义是将n号圆盘,或者说初始状态下从上面数第n个圆盘,从from移动到to)
第二步确定递归部分,其实就是N+1与N的关系部分,就是红色字体部分。现在开始把文字转化为程序:
设Hanoi (int n, int from, int temp, int to)函数就是我们要求的汉诺塔实现函数,意义是将按直径递增摞在一起的n个圆盘从from按要求移动到to上,temp为辅助圆盘。
可写出代码:
Hanoi (n-1, from, temp); /* 先将上面的N个移动到temp上 */
move (n, from, to); /* 剩下的最后一个移动到to上 */
Hanoi (n-1, temp, to); /* 再把temp上的移动到to上 */
第二步完成,最后合成函数:
void
Hanoi (int n, int from, int temp, int to)
{
if (n == 1)
move (n, from, to);
else
{
Hanoi (n-1, from, temp);
move (n, from, to);
Hanoi (n-1, temp, to);
}
}
使用数学归纳法设计递归程序最大的好处就是可以使设计者摆脱对递推的顾虑。因为你设计的代码必定隐含着递推的步骤。直接根据你的分析文字转化为代码即可。
本文还有很多不足之处,欢迎读者指教。