数据结构学习笔记——递归(分而治之)




什么是递归?

   在定义一个过程或函数时出现调用本过程或本函数的成分,称之为递归。若调用自身,称之为直接递归。若过程或函数p调用过程或函数q,而q又调用p,称之为间接递归。 
   如果一个递归过程或递归函数中递归调用语句是最后一条执行语句,则称这种递归调用为尾递归。

   以下是求n!(n为正整数)的递归函数。
复制代码
int  fun( int  n)
{    
 
if  (n == 1 )              /* 语句1 */
     
return   1 ;         /* 语句2 */
 
else               /* 语句3 */
     
return  fun(n - 1 ) * n;     /* 语句4 */
}
复制代码

   在该函数fun(n)求解过程中,直接调用fun(n-1)(语句4)自身,所以它是一个直接递归函数。又由于递归调用是最后一条语句,所以它又属于尾递归。

何时使用递归
     在以下三种情况下,常常要用到递归的方法。
     1. 定义是递归的(数学公式、数列等的定义)
     2. 数据结构是递归的
       如:单链表就是一种递归数据结构,其结点类型定义如下:
    
typedef  struct  LNode
{
  ElemType data;
  
struct  LNode  * next;      
} LinkList;
      该定义中,结构体LNode的定义中用到了它自身,即指针域next是一种指向自身类型的指针,所以它是一种递归数据结构。 

      对于递归数据结构,采用递归的方法编写算法既方便又有效。例如,求一个不带头结点的单链表head的所有data域(假设为int型)之和的递归算法如下:
    
复制代码
int  Sum(LinkList  * head)
{
  
if  (head == NULL)
   
return   0 ;
  
else
   
return (head -> data + Sum(head -> next));

复制代码

     3. 问题的求解方法是递归的
     对于汉诺塔问题:
     要想将N个盘子,借助于Y从X移到Z上,我们必须先n-1个盘子从X借助于Z移动到Y上,再将第N个盘子移动到Z上,然后再将n-1个盘子从Y借助于X移到Z上,这样我们的目的就达到了。
     为此其对应的Hanoi算法是
      
复制代码
#include < stdio.h >
      
int  i = 0 ;
      
void  Hanoi( int  n, char  x, char  y, char  z)
     {
        
if (n == 1 )
        {
              
           printf(
" %c  ------>  %c\n " ,x,z);
           i
++ ;
           
return ;
        }
        Hanoi(n
- 1 ,x,z,y);
        printf(
" %c  ------>  %c\n " ,x,z);
        i
++ ;
    Hanoi(n
- 1 ,y,x,z);
     }
     
int  main()
     {
       Hanoi(
7 , ' x ' , ' y ' , ' z ' );
       printf(
" 总共移动%d步 " ,i);
       getchar();
     }
复制代码

递归模型
       递归模型是递归算法的抽象,它反映一个递归问题的递归结构,例如,阶乘递归算法对应的递归模型如下:
       fun(1)=1                                (1)            
       fun(n)=n*fun(n-1)     n>1    (2)
      其中,第一个式子给出了递归的终止条件,第二个式子给出了fun(n)的值与fun(n-1)的值之间的关系,我们把第一个式子称为递归出口,把第二个式子称为递归体。

      一般地,一个递归模型是由递归出口和递归体两部分组成,前者确定递归到何时结束,后者确定递归求解时的递推关系。递归出口的一般格式如下:
       f(s1)=m1                                      (6.1)
      这里的s1与m1均为常量,有些递归问题可能有几个递归出口。递归体的一般格式如下:
       f(sn+1)=g(f(si),f(si+1),…,f(sn),cj,cj+1,…,cm)       (6.2)
      其中,n,i,j,m均为正整数。这里的sn+1是一个递归“大问题”,si,si+1,…,sn为递归“小问题”,cj,cj+1,…,cm是若干个可以直接(用非递归方法)解决的问题,g是一个非递归函数,可以直接求值。

      实际上,递归思路是把一个不能或不好直接求解的“大问题”转化成一个或几个“小问题”来解决,再把这些“小问题”进一步分解成更小的“小问题”来解决,如此分解,直至每个“小问题”都可以直接解决(此时分解到递归出口)。但递归分解不是随意的分解,递归分解要保证“大问题”与“小问题”相似,即求解过程与环境都相似。 

为了讨论方便,简化上述递归模型为:
 f(s1)=m1            (6.3)
 f(sn)=g(f(sn-1),c)        (6.4)
求f(sn)的分解过程如下:
    f(sn)
      ↓
    f(sn-1)
     ↓
    …
     ↓
    f(s2)
     ↓
    f(s1)

    一旦遇到递归出口,分解过程结束,开始求值过程,所以分解过程是“量变”过程,即原来的“大问题”在慢慢变小,但尚未解决,遇到递归出口后,便发生了“质变”,即原递归问题便转化成直接问题。上面的求值过程如下:
    f(s1)=m1
      ↓
    f(s2)=g(f(s1),c1)
      ↓
   f(s3)=g(f(s2),c2)
      ↓
      …
      ↓
   f(sn)=g(f(sn-1),cn-1)      

    这样f(sn)便计算出来了,因此,递归的执行过程由分解和求值两部分构成。 


递归与数学归纳法
    从递归体看到,如果已知si,s(i+1),...,sn,就可以确定s(n+1).从数学归纳法的角度来看,这相当于数学归纳步骤的内容,但仅有这个关系,还不能确定这个数列,若使它完全确定,还应给出这个数列的初始值S1,这相当于数学归纳法基础的内容。
    如:采用数学归纳法证明下式:
                                    n(n+1)
           1+2+... ...+n = ---------
                                        2

                                      1*2
    当n=1时,左式=1,右式=-------=1,左右两式相等,等式成立
                                        2

    假设当n=k-1时等式成立,有
                                            k(k-1)
               1+2+... ...+(k-1)=---------
                                               2

    当n=k时,
                                                                         k(k-1)            k(k+1)
             左式 = 1+2+...+k = 1+2+...+(k-1)+k = --------- + k = --------
                                                                           2                    2
    等式成立。即证。
    数学归纳法是一种论证方法,而递归是算法和程序设计的一种实现技术,数学归纳法是递归的基础。


递归算法的设计
       递归的求解的过程均有这样的特征:先将整个问题划分为若干个子问题,通过分别求解子问题,最后获得整个问题的解。而这些子问题具有与原问题相同的求解方法,于是可以再将它们划分成若干个子问题,分别求解,如此反复进行,直到不能再划分成子问题,或已经可以求解为止。这种自上而下将问题分解、求解,再自上而下引用、合并,求出最后解答的过程称为递归求解过程。这是一种分而治之的算法设计方法。
       递归算法设计先要给出递归模型,再转换成对应的C/C++语言函数。

递归设计的步骤如下:
      (1)对原问题f(s)进行分析,假设出合理的“较小问题”f(s')(与数学归纳法中假设n=k-1时等式成立相似);
      (2)假设f(s')是可解的,在此基础上确定f(s)的解,即给出f(s)与f(s')之间的关系(与数学归纳法中求证n=k时等式成立的过程相似);
      (3)确定一个特定情况(如f(1)或f(0))的解,由此作为递归出口(与数学归纳法中求证n=1时等式成立相似)。

例如,采用递归算法求实数数组A[0..n-1]中的最小值。
    假设f(A,i)函数求数组元素A[0]~A[i]中的最小值。
    当i=0时,有f(A,i)=A[0];
    假设f(A,i-1)已求出,则f(A,i)=MIN(f(A,i-1),A[i]),其中MIN()为求两个值较小值函数。因此得到如下递归模型:
                       A[0]                            当i=0时
        f(A,i)=
                       MIN(f(A,i-1),A[i])      其他情况

再如:利用串的基本运算写出对串求逆的递归算法。

    解:经分析,求逆串的递归模型如下:
                 s                                                     若s.length>1
       f(s)=
                Concat(f(SubStr(s,2,StrLength(s)-1)),SubStr(s,1,1))   其它情况

复制代码
#include < iostream >
#include
< string >
using   namespace  std;

string  invert( string   & s)
{
   
string  s1,s2,temp;
   
if (s.length() > 1 )
   {
        s1
= invert(temp = s.substr( 1 ,(s.length() - 1 )));
     s2
= s1.append(s.substr( 0 , 1 ));
   }
   
else
     s2 
=  s;
   
return  s2;
}
int  main()
{
 
string  s = " qwert " ;
 
string  f = invert(s);
 cout
<< f << endl;
}
复制代码


例 求1,2,…,n的全排列。
 解:用数组a存放n个不同字符的字符串,设f(a,k,n)为a[0..k]的所有字符的全排序。则f(a,k-1,n)为a[0..k-1]的所有字符的全排序。假设f(a,k-1,n)可求,对于a[k]位置,可以取a[0..k]任何之值,再组合f(a,k-1,n),则得到f(a,k,n)递归模型为:
  f(a,k,n):输出a                              当k=0时
  f(a,k,n):a[k]位置取a[0..k]任何之值,       其他情况
  并组合f(a,k-1,n)的结果;

//全排列算法
复制代码
#include < stdio.h >
int  _i;
void  f( char  a[], int  k, int  n)
{    
    
int  i,j;
    
if  (k == n - 1 )
    {     
for  (j = 0 ;j < n;j ++ )
            printf(
" %c " ,a[j]);
          _i
++ ;  
          printf(
" \n " );
    }
    
else
    {     
for  (i = k;i < n;i ++ )
          {
              
char  temp;
            temp
= a[i];
            a[i]
= a[k];
            a[k]
= temp;
            f(a,k
+ 1 ,n);
            a[k]
= a[i];
            a[i]
= temp;
            
          }
    }
 }
int  main()
{
   
char  a[] = { ' a ' , ' b ' , ' c ' , ' d ' , ' e ' , ' f ' };
   f(a,
0 , 6 );
   printf(
" %d " ,_i);

}
复制代码



例 求顺序表{a1,a2,...,an}中的最大元素
解:将线性表分解为{a1,a2,a3,...am}和{am+1,...,an}两个子表,分别求得子表中的最大元素ai和aj比较ai和aj中的大者,就可以求得整个线性表的最大元素。而求子表中的最大元素方法与总表相同,即再分别将它们分两个更小的子表,如此不断分解,直到表中只有一个元素为止,然后再向上,求出最大值。对应算法如下:

复制代码
#include < stdio.h >
int  Max( int  a[], int  i, int  j)
{
  
int  mid;
  
int  max,max1,max2;
  
if (i == j)
  {
   max
= a[i];
  }
  
else
  {
   mid
= (i + j) / 2 ;
   max1 
=  Max(a,i,mid);
   max2 
=  Max(a,mid + 1 ,j);
   max
= (max1 > max2) ? max1:max2;
  }
  
return (max);
}
int  main()
{
   
int  a[] = { 1 , 5 , 9 , 8 , 6 , 90 , 911 };
   printf(
" a=%d\n " ,(( int )Max(a, 0 , 6 )));

}
复制代码


例:试设计一个算法,求出左右子树的节点个数

复制代码
int  f(BTNode  * b)
{
  
if (b == null )
    
return   0 ;
  
else
  {
    
return (f(b -> lchild) + f(b -> rchild) + 1 );
  }
}
复制代码


采用递归算法求解皇后问题:在n×n的方格棋盘上,放置n个皇后,要求每个皇后不同行、不同列、不同左右对角线。
      解:设place(k,n)表示在前面1,…,k-1个皇后放置好后,用于放置k,…,n的皇后。求解皇后问题的递归模型如下:
        place(i,n):n个皇后放置完毕,输出解    若i>n
        place(k,n):对于第k列的每个合适的位置i,在其上放置一个皇后;
        place(k+1,n);                           其它情况
请自行实现算法。



递归算法到非递归算法的转换

    递归算法有两个基本特性:一是递归算法是一种分而治之的、把复杂问题分解为简单问题的求解问题方法,对求解某些复杂问题,递归算法分析问题的方法是十分有效的;二是递归算法的时间效率通常比较差。因此,对求解某些问题时,我们希望用递归算法分析问题,用非递归算法具体求解问题。这就需要把递归算法转换为非递归算法。

把递归算法转化为非递归算法有如下三种基本方法:
      (1)对于尾递归和单向递归的算法,可用循环结构的算法替代。
      (2)自己用栈模拟系统的运行时栈,通过分析只保存必须保存的信息,从而用非递归算法替代递归算法。
      (3)利用栈保存参数,由于栈的后进先出特性吻合递归算法的执行过程,因而可以用非递归算法替代递归算法。
      
      第(1)种和第(2)种情况的递归算法转化为非递归算法的问题,前者是一种是直接转化法,不需要使用栈,后者是间接转化法,需要使用栈。第(3)种情况也需要使用栈,但因具体情况而异,


尾递归和单向递归的消除

    尾递归是递归调用语句只有一个,而且是处于算法的最后。

    尾递归是单向递归的特例。单向递归是指递归函数中虽然有一处以上的递归调用语句,但各次递归调用语句的参数只和主调用函数有关,相互之间参数无关,并且这些递归调用语句,也与尾递归一样处于算法的最后。单向递归的典例就是Fibonacci数列。
    
    一般对于尾递归和单向递归可采用循环消除。(原因是,递归返回时,正好是算法的末尾,相当于保存的返回信息和返回值根本不需要被保存)
    
    采用循环结构消除递归没有通用的转换算法,对于具体问题要深入分析对应的递归结构,设计有效的循环语句进行递归到非递归的转换。
    
模拟系统运行时的栈消除递归(从现在开始培养解决问题运用递归的分而治之的思维并培养消除递归的能力)
    对于不属于尾递归和单向递归的递归算法,很难转化为与之等价的循环算法。但所有的递归程序都可以转化为与之等价的非递归程序。(例如,C/C++语言就是先将递归程序转化为非递归程序,然后求解的)。有一个通用的算法可以将递归程序转化为非递归程序,由于这个算法过于通用,比较复杂,不易理解,而且通常需要使用goto转移语句,违反结构化程序设计规定。(有空去看看)

   直接使用栈保存中间结果,从而将递归算法转化为非递归。

   在设计栈时,除了保存递归函数的参数外,还增加了一个标志成员(tag),对于某个递归小问题f(s`),其值为1表示对应的递归问题尚未求出,需进一步分解转换,值为0表示对应递归问题已求出,需通过该结果求解大问题f(s).

   为方便讨论,将递归模型分为等值关系和等价关系两种。

   等值关系:
       “大问题”的函数值等于“小问题”的函数值的某种运算结果,例如求n!对应的递归模型就是等值关系。

复制代码
int  fun( int  n)
{
   
if (n == 1 )
      
return   1 ;
   
else
      
return (fun(n - 1 ) * n);
}
复制代码


运用模拟系统运行时的栈消除递归重写该函数


Ackerman函数定义如下

                      n+1                  m=0
   akm(m,n)=  akm(m-1,1)           m!=0,n=0
                      akm(m-1,akm(m,n-1))  m!=0,n!=0


对于“等值关系”通用解决方案:
1、设计压栈数据元素
2、首先将初始压栈,tag初始为0(还可跟据需要,加入返回值等),如果有按条件返回的多个分支递归,可设置一个trans标记,然后压栈的时,转到对应的分支
3、当栈不为空时,循环(每次最栈顶元素判断是压栈过程还是解栈过程)
压栈存储参数,回归时期解决具体问题。

   等价关系:
       等价关系是指“大问题”的求解过程转化为“小问题”求解而得到的,它们之间不是值的相等关系,而是解的等价关系。

       例如,求梵塔问题对应的递归模型就是等价关系,也就是说,Hanoi(n,x,y,z)与Hanoi(n-1,x,z,y)、move(n,x,z)和Hanoi(n-1,y,x,z)是等价的。

复制代码
#include  < stdio.h >
#define  MaxSize 100
struct
{
    
int  n;         /* 保存n值 */
    
char  x;         /* 保存x值 */
    
char  y;         /* 保存y值 */
    
char  z;         /* 保存z值 */
    
int  tag;     /* 标识是否可直接操作,1:不能,0:可以 */
} St[MaxSize];
int  top =- 1 ;
void  Hanoi1( int  n, char  x, char  y, char  z)
{
    
int  n1,x1,y1,z1;
    
if  (n <= 0 )
    {    
        printf(
" 参数错误\n " );
        
return ;
    }
    
else   if  (n == 1 )
    {    
        printf(
" 将%c上的1个盘片直接移动到%c\n " ,x,z);
        
return ;
    }
    top
++ ;
    St[top].n
= n;   /* 初值进栈,n代表当前移动的盘子 */
    St[top].x
= x;
    St[top].y
= y;
    St[top].z
= z;
    St[top].tag
= 1 /* 不能直接操作 */
    
while  (top >- 1 )     /* 栈不空循环 */
    {    
        
if  (St[top].tag == 1   &&  St[top].n > 1 )    /* 当不能直接操作时 */
        {    
            n1
= St[top].n;         /* 退栈hanoi(n,x,y,z),并将当前移动的盘子数保存 */
            x1
= St[top].x; y1 = St[top].y; z1 = St[top].z;  /* 保存当前的移动操作 */
            top
-- ;               /* 退栈 */
            top
++ ;                 /* 将Hanoi(n-1,y,x,z)进栈,将n-1个盘从y借助于x移动到z上 */
            St[top].n
= n1 - 1 ;        
            St[top].x
= y1; St[top].y = x1; St[top].z = z1;  /* 改变各参数 */
            
if  (n1 - 1 == 1 )         /* 只有一个盘片时直接操作 */
                St[top].tag
= 0 ;
            
else
                St[top].tag
= 1 ;
            top
++ ;                 /* 将第n个圆盘从x移到z; */
            St[top].n
= n1;
            St[top].x
= x1; St[top].z = z1; St[top].tag = 0 ;
            
            top
++ ;                 /* 将Hanoi(n-1,x,z,y)进 */
            St[top].n
= n1 - 1 ;
            St[top].x
= x1; St[top].y = z1; St[top].z = y1;
            
if  (n1 - 1 == 1 )         /* 只有一个盘片时直接操作 */
                St[top].tag
= 0 ;
            
else
                St[top].tag
= 1 ;
        }
        
else   if     (St[top].tag == 0 )   /* 当可以直接操作时 */
        {     
            printf(
" \t将第%d个盘片从%c移动到%c\n " ,St[top].n,St[top].x,St[top].z);
            top
-- ;                 /* 移动盘片后退栈 */
        }
    }
}

void  main()
{
    
int  n = 3 ;
    printf(
"   非递归求解结果:\n " );
    Hanoi1(n,
' X ' , ' Y ' , ' Z ' );
}
复制代码


本算法与递归等值关系的异同点:
本算法只需访问节点,而等值算法只有到最后(栈中只一个元素)时才能求出函数值(不用返回参数等)。另外等值关系中递归模型的式子都是独立的,而本算法中三个部分是相关联的。

用递归算法实现背包问题求解

你可能感兴趣的:(数据结构学习笔记——递归(分而治之))