昨夜飘风
昨 夜, 风, 飘 过; 枯 树, 叶, 飞 落。
       如果一个算法调用自己来完成它的部分工作,就称这个算法是递归的。这种方法要想取得成功,必须在比原始问题小的
问题上调用自己。一个递归算法必须有两个部分:初始情况和递归部分。

什么是递归? 我们先举一个简单的例子来说明.我们知道,在数学中N!一般定义为:N=1*2*3*…*(N-1)*N 但也可以用下面公式来定义:
    { 1 若N=0
 N!={
    { N*(N-1) 若N>0
    在N>0的公式中,又包括了(N-1)!,这就是N!的递归定义.
    一般说,如果一个对象部分的有自己组成,或者是按他自己定义的,则称之为是递归的.
递归在数学和计算机学科中经常遇到.利用递归方法可以用有限的语句来定义无限集合,但在递归定义中至少要有一条是非递归的,即初始定义.否则就会产生逻辑性错误.


    在计算机科学中,递归概念还经常用于递归调用方面,即函数或过程自己调用自己.用递归调用的算法就是递归算法. 递归调用会产生无终止运算的可能性,因此必须在适当的情况下终止递归调用(也叫做边界条件).根据递归定义设计的递归算法中,非递归的初始定义就用做程序的终止条件.

递归与回溯

[算法分析]
    为了描述问题的某一状态,必须用到它的上一状态,而描述上一状态,又必须用到它的上一状态……这
种用自已来定义自己的方法,称为递归定义。例如:定义函数f(n)为:
        /n*f(n-1) (n>0)
f(n)= |
        " 1(n=0)
    则当0时,须用f(n-1)来定义f(n),用f(n-1-1)来定义f(n-1)……当n=0时,f(n)=1。
    由上例我们可看出,递归定义有两个要素:
    (1)递归边界条件。也就是所描述问题的最简单情况,它本身不再使用递归的定义。
         如上例,当n=0时,f(n)=1,不使用f(n-1)来定义。
    (2)递归定义:使问题向边界条件转化的规则。递归定义必须能使问题越来越简单。
         如上例:f(n)由f(n-1)定义,越来越靠近f(0),也即边界条件。最简单的情况是f(0)=1。

    递归算法的效率往往很低, 费时和费内存空间. 但是递归也有其长处, 它能使一个蕴含递归关系且结构
复杂的程序简介精炼, 增加可读性. 特别是在难于找到从边界到解的全过程的情况下, 如果把问题推进一步
没其结果仍维持原问题的关系, 则采用递归算法编程比较合适.

    递归按其调用方式分为: 1. 直接递归, 递归过程P直接自己调用自己; 2. 间接递归, 即P包含另一过程
D, 而D又调用P.

    递归算法适用的一般场合为:
    1. 数据的定义形式按递归定义.
       如裴波那契数列的定义: f(n)=f(n-1)+f(n-2); f(0)=1; f(1)=2.
       对应的递归程序为:
         Function fib(n : integer) : integer;
         Begin
            if n = 0 then fib := 1     { 递归边界 }
                     else if n = 1 then fib := 2
                                   else fib := fib(n-2) + fib(n-1)   { 递归 }
         End;
       这类递归问题可转化为递推算法, 递归边界作为递推的边界条件.
    2. 数据之间的关系(即数据结构)按递归定义. 如树的遍历, 图的搜索等.
    3. 问题解法按递归算法实现. 例如回溯法等.

    从问题的某一种可能出发, 搜索从这种情况出发所能达到的所有可能, 当这一条路走到" 尽头 "
的时候, 再倒回出发点, 从另一个可能出发, 继续搜索. 这种不断" 回溯 "寻找解的方法, 称作
" 回溯法 ".

[参考程序]
    下面给出用回溯法求所有路径的算法框架. 注释已经写得非常清楚, 请读者仔细理解.
Const maxdepth = ????;
Type statetype = ??????; { 状态类型定义 }
     operatertype = ??????; { 算符类型定义 }
     node = Record { 结点类型 }
               state : statetype; { 状态域 }
       operater :operatertype { 算符域 }
    End;
{ 注: 结点的数据类型可以根据试题需要简化 }
Var
   stack : Array [1..maxdepth] of node; { 存当前路径 }
   total : integer; { 路径数 }
Procedure make(l : integer);
Var i : integer;
Begin
   if stack[L-1]是目标结点 then
   Begin
      total := total+1; { 路径数+1 }
      打印当前路径[1..L-1];
      Exit
   End;
   for i := 1 to 解答树次数 do
   Begin
      生成 stack[l].operater;
      stack[l].operater 作用于 stack[l-1].state, 产生新状态 stack[l].state;
      if stack[l].state 满足约束条件 then make(k+1);
      { 若不满足约束条件, 则通过for循环换一个算符扩展 }
      { 递归返回该处时, 系统自动恢复调用前的栈指针和算符, 再通过for循环换一个算符扩展 }
      { 注: 若在扩展stack[l].state时曾使用过全局变量, 则应插入若干语句, 恢复全局变量在
                        stack[l-1].state时的值. }
   End;
   { 再无算符可用, 回溯 }
End;
Begin
   total := 0; { 路径数初始化为0 }
   初始化处理;
   make(l);
   打印路径数total
End.

[例子]    求N个数的全排列。
    [分析]求N个数的全排列,可以看成把N个不同的球放入N个不同的盒子中,每个盒子中只能有一
个球。解法与八皇后问题相似。
    [参考过程]
       procedure try(I:integer);
       var j:integer;
        begin
             for j:=1 to n do
                  if a[j]=0 then
                 begin
                      x[I]:=j;
                      a[j]:=1;
                      if I<n then try(I+1)
                         else print;
                      a[j]=0;
                 end;
        end;

前面我们比较详细的学习了递归程序的设计方法,有些同学可能会提出,许多问题不是用递归算法,照样也能解决,想前面介绍的求阶乘的程序,在刚开始学习程序设计的时候就已经解决各类似问题,算法也不是很难。这就是递归与递推两种程序设计方法。

递归:通过前面学习,我们知道递归在解决问题时,首先从问题的最终结果入手,经过一系列的调用,得到最原始的结果,在逐步返回,最后得出最终结果。如2.1节中的阶乘函数factorial(n),要得到n!,需要调用factorial(n-1)计算(n-1)!,同样要计算(n-1)!需要计算(n-2)!,这样下去一直调用到factorial(0),得到结果1,在逐步返回求解2!、3!……,一直返回到最初调用处,计算出n!。

递推:与递归相比,递推方法恰恰相反,它是从问题的最底层向最终结果推导。还是以求n!为例,先从0!开始,计算1!(1*0!)、2!(2*1!)、……,一直推导到n!。

 

从程序的设计执行效率来讲,递归算法的效率往往很低,这是因为递归是一种特殊的函数嵌套调用(调用自身),在进行一系列自身调用的同时,操作系统需要进行大量的工作栈地压栈操作,在返回时又需要一系列的出栈操作,费时又费内存空间。相对来说,递推在这方面的优越性较高,一般不需要类似的函数嵌套调用,经过一系列的循环推导即可达到目的。

但是递归也有其很多的长处, 它能使一个蕴含递归关系且结构复杂的程序简洁精炼,提高变成效率,把一些复杂的操作交给计算机去自动完成,同时增加了程序可读性。一般来讲,能使用递归的程序都可以使用递归来完成。但是在难于找到底层结果地情况下,则采用递归算法编程比较合适。如汉诺塔移动问题,如果使用递推其算法相当复杂,几乎无法实现,因为你很难直接得到第一步的移动方法。类似问题还有八皇后问题、快速排序等。递归就是以系统的资源为代价换取算法的简单化。

 

下面以求最大公约数为例,比较一下递归与递推两种算法
   

递归算法

 

递推算法

gcd(int m,int n)

{

   if(n==0)

      return (m);

   else

      return (gcd(n,m%n));

}

 

gcd(int m,int n)

{

   int v;

   while (n!=0) {

      v=m%n;

      m=n;

      n=v;

      }

   return (m);

}

posted on 2008-04-08 14:26  昨夜飘风  阅读(1874)  评论(0编辑  收藏  举报