第二章——递归

本文详细介绍了递归的概念、模型、执行过程以及如何设计递归算法,包括递归的数学归纳法、递归数据结构如单链表和二叉树的递归算法设计。此外,还探讨了如何将递归算法转化为非递归算法,以及递推式的计算方法。通过实例分析,如求解斐波那契数列、释放和复制二叉树节点、求解最大元素和末尾零个数等,帮助读者深入理解递归在算法设计中的应用。
摘要由CSDN通过智能技术生成

递归的定义,递归算法,递归模型,递归栈,递归树

在数学和计算机科学中,递归是指在在一个过程或函数的定义时出现调用本过程或本函数的成分。

若在函数中调用函数自身或者在过程的子部分中调用子部分自身的内容,称之为直接递归,又称自递归。若不同的函数和子过程之间互相调用,则称之为间接递归,任何间接递归都可以等价地转换为直接递归(自递归),所以我们讨论递归一般只讨论直接递归。

如果一个递归过程或递归函数中,递归调用语句是最后一条执行语句,则称这种递归调用为尾递归。

一个有趣且经典的介绍递归的例子如下:

递归的定义:请查阅“递归的定义”。

这个例子不仅很好的体现了递归的特性,还同时满足自递归和尾递归,但如果在算法中使用该函数,从算法基本性质的角度而言就缺乏有限性。

递归算法(何时使用递归)

使用递归函数(过程)的算法就称为递归算法,递归算法中的递归函数(过程)需要满足算法的特性,所以递归算法中的递归函数需要满足以下几个条件:

1.递归过程中进行递归调用的次数必须是有限的。

2.必须有结束递归的条件来终止递归。

这两个条件保证了递归算法能够被结束,即递归算法的有限性。

同时从被求解问题的角度来看,需要解决的问题需要能够被“分解”成“规模较小的问题”,即可以转化为一个或多个子问题(规模较小的阶段)来求解。

我们只讨论自递归算法(所有递归算法都可以转化为自递归算法),可以发现子问题(阶段)的求解策略,求解环境与原问题基本类同,只是在具体的执行过程中因为某些与问题(阶段)相关的属性上的差异导致执行指令的不同(策略是相同的),从自递归的角度来看,递归思想就是将一个复杂的大问题逐渐转化成类同的“小问题”以及从“小问题”到“大问题”需要进行的操作。

以上即是递归使用在算法中的前提条件。

递归模型

递归模型是递归算法的抽象,它反映了一个递归问题的递归结构。

对于递归算法中的递归,递归的终止条件称之为递归出口,规模较大的问题(阶段)与规模较小的问题(阶段)之间的桥梁,即从“小问题”的解到“大问题”的解之间需要进行的操作(往往是数学公式)称为递归体。

一般地,一个递归模型由递归出口和递归体两部分,前者确定递归到何时结束,即指出明确的递归结束条件,后者确定递归求解时地递推关系。

递归出口的一般格式如下:

f(s1)=m1。

s1表示递归结束时的子问题(阶段),有些递归问题中可能有多个递归出口。递归体的格式如下:

f(s(n+1))=g(f(s(a1)),f(s(a2))·······,f(s(ak)),c1,c2······c(t))。

s(i)为递归过程中分解出来的子问题,c(i)为递归过程中连接规模较小的问题到规模较大的问题的桥梁,即可以直接(用非递归方法)解决的问题,f为递归函数,g为一个阶段的非递归求值函数。

递归算法的执行过程

从指令序列的角度来看,递归算法在执行递归函数时会不断地调用自身,但仅有调用自身的操作而不发生变化会让程序无休止地调用而陷入死循环,即从思想的角度来看规模较大的问题没有分解成规模较小的问题。

一个正确的递归程序虽然每次调用的是相同的子程序,但它的参量,输入数据等均有可能发生变化。在正常的情况下,随着调用的不断深入,必定会出现调用到某一层的函数时,不再执行递归调用而终止函数的执行,遇到递归出口便是这种情况。

递归调用是函数嵌套调用的一种特殊情况,即它是调用自身代码。也可以把每一次递归调用理解成调用自身代码的一个复制件。由于每次调用时,它的参量和局部变量均不相同,因而也就保证了各个复制件执行时的独立性。

系统为每一次调用开辟一组存储单元,用来存放本次调用的返回地址以及被中断的函数的参量值。这些单元以系统栈的形式存放,每调用一次进栈一次,当返回时执行出栈操作,把当前栈顶保留的值送回相应的参量中进行恢复,并按栈顶中的返回地址,从断点继续执行。

例 2.1

求n!的递归算法(保证输入的n为正整数)如下,分析执行fun(5)时系统栈的内部情况。

int fun(int n){
   
	//递归出口  
	if (n==1) return(1);
	//递归体		
   else return(fun(n-1)*n);	
}

执行fun(5)时,进入else执行到fun(4),将fun(4)*5进栈:

在这里插入图片描述

因为没有返回值,所以一直调用进栈到fun(1)。

在这里插入图片描述

在这里插入图片描述

进行到fun(1)时,返回1,执行出栈,并将出栈的也就是栈顶的值恢复到fun(2)中进行运算:

在这里插入图片描述

直到栈为空时,执行完毕:

在这里插入图片描述

从以上过程可以得出:

1.递归是通过系统栈来实现的
2.每递归调用一次,就需进栈一次。最多的进栈元素个数称为递归深度,一般而言n越大,递归深度越深,开辟的栈空间也越大,如果进站元素过多则会出现栈溢出的情况。
3.每当遇到递归出口或完成本次执行时,需退栈一次,并将退栈的值恢复到原本的位置中,当全部执行完毕时,栈应为空。

程序执行和递归思想的角度结合来看,进栈的过程可以看作分解问题的过程,退栈恢复的过程看作问题求值的过程,能够得到以下的递归树:

在这里插入图片描述

这颗递归树的结构比较简单,我们看另外一个复杂一点的问题。

例 2.3

斐波那契数列定义如下:

Fib(n)=1,n=1;
Fib(n)=1,n=2;
Fib(n)=Fib(n-1)+Fib(n-2),n>2。

对应的递归算法如下:

int Fib(int n){
     
	if (n==1||n==2) return 1;
  else return Fib(n-1)+Fib(n-2);
}

画出求Fib(5)的递归树以及递归工作栈的变化和求解过程。

Fib(5)分解成Fib(4)和Fib(3),Fib(4)分解成Fib(3)和Fib(2),Fib(3)分解成Fib(2)和Fib(1),Fib(2)和Fib(1)返回值退栈,恢复到Fib(3),再从Fib(2)和Fib(3)恢复到Fib(4),最后得到Fib(5)的值。

黑色的线表示分解过程,紫色的表现表示求解过程,求解Fib(5)的递归树如下:

在这里插入图片描述

执行Fib(5)时递归工作栈的变化和求解过程:

在这里插入图片描述

可以发现例如Fib(3)在整个过程中是重复计算的,我们可以将Fib(3)的值存储下来,当递归到Fib(3)时直接返回存储的值,有关到后面动态规划的内容,这里就不多赘述了。


递归算法设计

递归与数学归纳法

递归算法分解问题的过程和和数学归纳法是非常类似的,虽然目的和性质不同(从实现上看,递归是算法和程序设计中为了简化代码,简化结构的一种实现技术,数学归纳法是数学问题证明的一种理论模型),我们可以将数学归纳法看作用递归设计算法的理论基础。

即我们用递归来设计算法,可以用数学归纳法作为理论基础来证明我们设计的递归算法是能够达成我们的目标的。

数学归纳法分为第一归纳原理和第二归纳原理。

第一数学归纳法

第一数学归纳法原理:

若{P(1),P(2),P(3),P(4)······}是命题序列且满足以下两个性质,则所有命题均为真:

1.P(1)为真。
2.任何命题均可以从它的前一个命题推导得出。

例如证明1到n的求和结果为n(n+1)/2。

1.当n=1时,左式=1,右式=1*(1+1)=1,左右两式相等,等式成立。
2.假设当n=k-1时等式成立,有1+2+······+(k-1)=(k-1)k/2。
当n=k时,根据n=k-1的结论,左式=1+2+…+k=1+2+…+(k-1)+k=(k-1)k/2+k=k(k+1)/2,满足n=k时的结论。

根据第一数学归纳法和结论1,2,问题即证。

第二数学归纳法

第二数学归纳法原理:

若{P(1),P(2),P(3),P(4),······}是命题序列且满足以下两个性质,则所有命题均为真:

1.P(1)为真。
2.任何命题均可以从它前面的所有命题推导得出。

条件2的意思是P(i)可以从前面所有命题假设{P(1),P(2),P(3),…,P(i-1)}推导得出。

例如,采用第二数学归纳法证明,任何含有n(n≥0)个不同结点的二又树,都可由它的中序序列和先序序列唯一地确定。

1.n等于0时,结论显然成立。
2.假设结点数小于n且所有结点值不同的任何二叉树,都可以由其先序序列和中序序列唯一地确定。

通过结点个数为n的二叉树的先序遍历结果,我们可以找到该二叉树的根节点(先序遍历的第一个结点为根结点),由于结点值不同,我们可以通过根节点的值在该二叉树的中序序列找到根结点的位置,然后将中序序列分割成两个左右子树对应的中序序列,根据左右子树对应的中序序列的长度可以在先序序列中找到左右子树对应的先序序列。

这样我们就有了左右子树对应的中序序列和先序序列,左右子树的结点个数显然小于n,根据归纳假设的条件,左右子树可以由其先序序列和中序序列唯一地确定。于是这颗结点个数为n的二叉树也唯一地确定了。

根据第二数学归纳法和结论1,2,问题即证。

递归算法设计的一般步骤

一般步骤就是去建立递归模型,然后用程序去实现它。

获取递归模型的步骤如下:

1.对原问题进行分析,分解出合理的较小阶段,即划分阶段(得到数学归纳法中一连串的命题序列)。

2.假设较小阶段对应的问题是可解的,在此基础上确定原问题(较大问题)的解,即求解出命题之间的关系(相当于归纳假设前面的命题成立,然后通过归纳假设的结论的基础上来证明当前命题成立)。

3.确定一个较小的阶段直接求(返回)解,由此作为递归出口(相当于数学归纳法中,求证n=1或n=0时等式成立)。

例 2.5

用递归法求一个整数数组a的最大元素。

思考建立求解整数数组a最大元素的递归模型(第一数学归纳法):

我们想到以元素的个数来划分阶段,设f(a,i)求解数组a中前i个元素的最大元素,则f(a,i-1)求解数组a中前i-1个元素的最大元素,前者为规模较大的问题,后者为规模较小的问题,f(a,n)为我们想要求解的原问题。

假设f(a,i-1)已求出,则有f(a,i)=MAX{f(a,i-1),a[i-1]}。当我们向左递推到a中只剩下一个元素时,最小元素一定为该元素,返回该元素即可,于是递归出口为:f(a,1)=a[0]。

对应的递归算法如下:

int fmax(int a[],int i){
      
	if (i==1) return a[0];//递归出口 
    else return(fmax(a,i-1),a[i-1]);//递归体 
}

前面提到能够用递归解决的问题应该满足以下三个条件:

1.需要解决的问题可以转化为一个或多个子问题(规模较小的阶段)来求解,而这些子问题(阶段)的求解策略与原问题完全相同,只是在某些与问题(阶段)相关的属性上不同(策略的细节会因为属性的不同而改变)。

2.递归调用的次数必须是有限的。

3.必须有结束递归的条件来终止递归。

这三个条件是笼统地介绍可以递归可以使用的地方,但是像很多问题,例如上面求解一个数组的最大元素,它可以用递归设计算法,但是这样用递归设计出来的算法就像为了使用递归而用递归设计一样,递归简化代码的特性没能够体现出来,这样使用递归在算法设计中显然是不够灵活的。

所以我们还需要讨论一些经常要用到递归和适合用递归的地方。

递归数据结构及其递归算法设计

采用递归方式定义(数据结构结构体的成员类型是数据结构本身或数据结构本身对应的指针)的数据结构称为递归数据结构,在递归数据结构定义中包含的递归运算称为基本递归运算。

例如单链表就是一种递归数据结构,结构体的成员变量next的数据类型为结构体Node本身对应的指针:

typedef struct LNode{
      
	ElemType data;
    struct LNode *next;	  
}LinkList;

对于任一单链表结点t而言,取其下一个结点t->next就是基本递归运算。

归纳起来,递归数据结构的定义如下:

RD={D,Op}

其中,D为构成该数据结构的所有元素的集合,Op是基本递归运算的集合,Op中的基本递归运算对于集合D存在封闭性。

基于递归数据结构的递归算法设计

递归算法设计建立递归模型最重要的步骤是对于分解出规模较小的问题。对于递归数据结构,分解问题的过程要明显和简单很多,因为从定义上而言本来就是递归的,直接通过递归数据结构中的基本递归运算来分解规模较小的问题即可。

单链表的递归算法设计

对单链表L进行递归算法设计的一般步骤如下:

1.设求解为以L结点指针为首结点指针的整个单链表的问题为规模最大的“原问题”。
2.设求解为以L->next为首结点指针的单链表的问题为规模较小的“子问题”。
3.由“大问题”的解和“小问题”的解之间的关系得到递归体(假设“子问题”的完成是前提,思考如何在小问题完成的前提下完成大问题)。
4.再考虑特殊情况:通常是单链表为空或者只有一个结点时,此时往往能够直接返回问题的解,从而得到递归算法的递归出口。

例 2.6

有一个不带头结点的单链表L,设计一个算法释放其中所有结点。

释放以L为首结点指针的单链表的所有节点为规模最大的原问题,释放以L->next为首结点指针的单链表的所有节点为规模较小的子问题。

假设以L->next为首结点指针的单链表的所有节点已经全部被释放,我们再将释放L指向的空间就相当于释放了以L为首结点指针的单链表的所有节点。

我们设f(t)为释放以t为首结点指针的单链表的所有节点,则f(L)就可以采用先调用f(L->next),然后释放L指向的空间来求解。

在这里插入图片描述

当想要释放的单链表为空时,不用进行另外的操作,整个单链表释放结束,即为该递归算法的递归出口。

对应的递归算法如下:

//释放单链表L中所有结点
void DestroyList(LinkNode *&L){
     
	if (L!=NULL)
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值