您的位置:首页 > 其它

典型算法及应用——“递归法”探究

2011-09-14 23:12 260 查看



在起始条件已知的情况下,解决一类重复性问题的最佳方案莫过于使用程序设计的三大基本结构之一的“循环”结构(分为“有限次”和“无限次”循环两种情况)。然而现实生活中也存在这样一类问题——起始条件不明确,但结尾却已知;或者问题自身嵌套着自身。在这种情况下我们将采用反向思维,从结尾条件开始往前推演,直到把起始条件推算出为止。这样的一种算法往往被成为“递归”法。本章节将主要对此算法进行一系列的探究。

一、递归法的定义和数学模型:

递归法在数学上的表达函数有点特殊,它是一种自调用函数,形式如下:



从定义式可以看出递归函数的最大特征在于自身的输出作为自身的输入,直至输入为某一个条件而终止。

下面就结合实践,具体阐述并验证这一理论。

【例1】有甲、乙、丙、丁四人,从甲开始到丁,一个比一个大1岁,已知丁10岁,问甲几岁?

【分析】这是递归法的一道非常典型的题目——因为我们可以很显然知道:假设要计算甲的年龄,那么必须直到乙的年龄;同样,算乙的必须直到丙的,算丙的必须知道丁的,因为丁已知,自然可以往前推算了。现在假设有一个数学模型(函数)可以计算出他们各自的年龄(方便期间我们给他们编号——甲=1,乙=2,丙=3,丁=4),那么存在这一个F(X)函数,X表示某人的编号,其规律如下:

F(1)=F(2)+1

F(2)=F(3)+1

F(3)=F(4)+1

F(4)=10

显然,直到X=4的时候是一个终止值,其余情况下都是返回F(X’),F(X’’)……F(X’’……’),且前者总是比后至大1,这也符合了X’和X总是呈现一定函数关系(设想一下,如果不是等差和等比,又怎么可能在一个递归函数中进行计算?要知道,函数本身就是一个公式表示,既然是公式,那么一定是一种函数关系Y=F(X)),此处显然X和X’的关系是X=X’+1。

根据规律式,我们可以写出该递归函数:

int AgeCal(int id)

{

if(id==4) return 10;

else

return (AgeCal(id+1)+1);

}

【例2】计算n!

【分析】虽然这道题目不像例1一样清晰明了告诉你使用“递归”法反推,但是我们有这样一个常识——n!=(n-1)!*n;(n-1)!=(n-2)!*(n-1)……n=0或1,返回1.

显然n与n-1,n-2也是线性的递减数列(等差关系)。其规律如下:

F(n)=F(n-1)*n

F(n-1)=F(n-2)*(n-1)

F(n-2)=F(n-3)*(n-2)

……

F(1)=1或者F(0)=1(防止别人直接输入0)

编写其递归函数,如下:

int Fac(int n)

{

if(n==1 || n==0)

{

return 1;

}

else

return Fac(n-1)*n;

}

从例1、2可以知道,递归函数编写非常清晰明了,且结构简单轻快,难点在于发现递归函数,一旦递归函数发现,只要对应翻译成某种程序语言的代码就可以了。

【例3】求一组整数中的最大(小)值(整数是一个int[]数组,个数未知)。

【分析】当数字只有两个的时候,我们可以使用>和<直接比较;但是当数字超过2个的时候(假设3个),那么我们可以使用一个预订的函数(比如Max(1,2)和3进行比较),由于1,2两个数比较的时候已经得到一个最大值,因此在回代到Max中又变成了两个数的比较。这样,我们可以发现一个规律:

F(1,2,3,4……n)=F(1,2,3,4……n-1)和n比较

F(1,2,3,4……n-1)=F(1,2,3,4……n-2)和n-1比较

……

F(1,2,3)=F(1,2)和3比较

F(1,2)=结果(并回代)

相应的递归函数如下(C#):




Code

int Max(int[]numbers)

{

if(numbers.Length==2)

{

return (numbers[0]>numbers[1]?numbers[0]:numbers[1]);

}

else

{

int[]tempnumbers=new int[numbers.Length-1];

for(int i=0;i<numbers.Length-1;++i)

{

tempnumbers[i]=numbers[i];

}

return (Max(tempnumbers)>numbers[numbers.Length-1]? Max(tempnumbers): numbers[numbers.Length-1]

}

}

【例4】计算下列算式(共n项,n由输入决定)

2/1+3/2+5/3+8/5+……+A/B+(A+B)/A+……

【分析】本算式有两部分组成——分母和分子,我们逐一分析:

首先看分母:第一项是1(我们把“分子/分母”看成一个项),第二项是第一项的分子,第三项是第二项的分子……第n项是第n-1项分子,那么我们可以写出这样一个递归函数:

double GetFenMu(int n)

{

if(n==1)return 1;

else

{

return GetFenZi(n-1);

}

}
再看分子,第一项是2,第二项是第一项的分母+分子……第n项是n-1项的分母和分子的和,递归函数自然如下:

double GetFenZi(int n)

{

if(n==1)return 2;

else

{

return GetFenZi(n-1)+GetFenMu(n-1);

}

}
主程序调用的时候,只需要外部一个大循环即可:

for(int i=1;i<=n;++i)

{

sum+=GetFenZi(i)+GetFenMu(i);

}
例4的递归函数有两个,它们相互调用,直到彼此双方完成返回结果为止。相比较例1~例3而言,这种调用方法称为“间接递归”。因此,我们这就引出“间接递归”的定义——

若存在着F(X)和G(Y)两种函数,它们分别以对方的输出作为自己的输入,直到X或者Y满足某个特定常数为止,这样的递归称为“间接递归”;同样地,后者的输入X’(Y’)必须和前者X(Y)呈现固定的函数关系。

*二、递归和递推的关系探究(猜测,草案,可供参考):

我们知道,递归是递推的反思维。那么现在我们感兴趣的问题在于——递归和递推之间是不是有一个普遍使用的关系式(函数)?

为了实现这个目标,我们不妨先来探究下计算机内部究竟在递归的时候干了什么?数据存储的结构是怎样的?这将有助我们进一步构建递归到递推算法规律的发现。我们还是以例1作为引证,再次观察以下式子:

F(1)=F(2)+1

F(2)=F(3)+1

F(3)=F(4)+1

F(4)=10

我们发现,当主函数调用F函数的时候(输入1),程序开始转入F函数内部,带着参数“1”进入函数体进行计算。如果该函数不是一个递归函数,那么它应该直接返回一个结果给主函数,但是它在不满足终止条件的时候触发了自身(进入F(2)),此时,因为F(1)的结果尚未得出,但是计算机主进程不得不转入F(2)的函数计算,此时它将在堆栈区域开辟一个地址,存放F(1)函数的计算结果(是一个式子F(2)+1),此时主进程在进入F(2),接着又要如法炮制,直到F(4) 得到圆满结果,主进程将根据先前堆栈中的指针以此将结果弹出,直到最底层的那个F(1),求解完毕。所以递归的本质是一个压栈和出栈的过程。

根据这个原理,我们使用“压栈-出栈”方式解决例1问题(使用C#语言)

【分析】首先我们应该为每一个人创建一个对象,以便保持前次的状态;接着我们创建这些对象,逐一将他们放入堆栈中(当到达第四个人的时候,给出初值10),接着在出栈的过程中,使用临时变量记录每一个出栈的数值,并以此进行递增(+1)。下面是源码:
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐