什么是递归
递归的定义
在定义一个过程或函数时出现调用本过程或本函数的成分,称之为递归。若调用自身,称之为直接递归。若过程或函数p调用过程或函数q,而q又调用p,称之为间接递归。
任何间接递归都可以等价的转换为直接递归。
如果一个递归过程或递归函数中递归调用语句是最后一句执行语句,则称这种递归调用为尾递归。
例:设计求n!的递归算法。
int fun(int n)
{
if(n==1)
return(1);
else
return(fun(n-1)*n);
}
直接调用fun(n-1),所以是一个直接递归函数。且是最后一条语句,所以又属于尾递归。
能够用递归解决的问题应该满足以下三个条件:
- 需要解决的问题可以转换为一个或多个子问题求解,而这些子问题的求解方法与原问题完全相同,只是在数量规模上不同。
- 递归调用的次数必须是有限的。
- 必须有结束递归的条件来终止递归。
何时使用递归
在以下三种情况下,常常用到递归的方法。
1. 定义是递归的
有许多数学公式、数列等的定义是递归的。例如,求n!和Fibonacci数列等。可以将其递归定义直接转化为对应的递归算法。
2. 数据结构是递归的
有些数据结构是递归的。例如单链表,其结点类型声明如下:
typedef struct LNode
{
ElemType data;
struct LNode *next;
}LinkList;
结构体LNode的定义中,指针域next是一种指向自身类型的指针,所以它是一种递归数据结构。
对于递归数据结构,采用递归的方法编写算法既方便又有效。例如,求一个不带头结点的单链表L的所有data域(假设为int型)之和的递归算法如下:
int Sum(LinkList *L)
{
if(L==NULL)
return 0;
else
return(L->data+Sum(L->next));
}
例:分析二叉树的二叉链存储结构的递归性,设计求非空二叉链bt中所有结点值之和的递归算法,假设二叉链的data域为int型。
// 二叉树的结点类型定义如下
typedef struct BNode
{
int data;
struct BNode *lchild,*rchild;
}BTNode;
int Sumbt(BTNode *bt)
{
if(bt->lchild==NULL && bt->rchild==NULL)
return bt->data;
else
return (Sumbt(bt->lchild)+Sumbt(bt->rchild)+bt->data);
}
3. 问题的求解方法是递归的
典型的由Hanoi问题求解。
规则:每次只能移动一个盘片;盘片可以插在X,Y和Z中任一塔座;任何时候都不能将一个较大的盘片放在较小的盘片上。
将Hanoi(n,x,y,z)表示将n个盘片从x通过y移动到z上,递归分解的过程是:
Hanoi(n,x,y,z){
Hanoi(n-1,x,z,y);
move(n,x,z); // 将第n个圆盘从x移动到z
Hanoi(n-1,y,x,z);
}
递归模型
递归模型是递归算法的抽象,它反映一个递归问题的递归结构。
例如求n!的递归算法对应的递归模型:
fun(1) = 1 n=1 (1)
fun(n)=n*fun(n-1) n>1 (2)
(1)式给出递归的终止关系,(2)式给出fun(n)的值与fun(n-1)的值之间的关系,(1)式称为递归出口,(2)式称为递归体。
一个递归模型是由递归出口和递归体两部分组成。
递归出口的一般格式如下:
f(s1)=m1
s1和m1均为常量,有些递归问题可能有几个递归出口。
递归体的一般格式如下:
f(Sn+1)=g(f(Si),f(Si+1),...,f(Sn),cj,cj+1,...,cm)
sn+1是一个递归的大问题,si,si+1,…,sn是递归的小问题,cj,cj+1,…,cm是若干个可以直接(用非递归)解决的问题,g是一个非递归函数,可以直接求值。
递归算法的执行过程
- 递归程序虽然每次调用的是相同的子程序,但它的参量、输入数据等均有变化。
- 随着调用的不断深入,必定会出现调用到某一层的函数时,不再执行递归调用而终止函数的执行,即遇到递归出口。
- 递归调用时函数嵌套调用的一种特殊情况,即它是调用自身代码。
- 由于每次调用时,它的参量和局部变量均不相同,因而也就保证了各个复制件执行时的独立性。
- 系统为每一次调用开辟一组存储单元,用来存放本次调用的返回地址以及被中断的函数的参量值
- 这些单元以系统栈的形式存放,每调用一次进栈一次,当返回时出栈,把当前栈顶保留的值送回相应的参量中进行恢复,并按栈顶中的返回地址,从断点继续执行。
从以上过程可以得出:
- 每递归调用一次,就需进栈一次,最多的进栈元素个数称为递归深度,当n越大,递归深度越深,开辟的栈空间也越大。
- 每当遇到递归出口或完成本次执行时,需退栈一次,并恢复参量值,当全部执行完毕时,栈应为空。
- 在递归函数执行时,形参会随着递归调用发生变化,但每次调用后会恢复为调用前的形参,将递归函数的非引用型形参的取值称为状态。
- 递归函数的引用型形参在执行后会回传给实参,有时类似全局变量,不作为状态的一部分。
递归算法设计
递归与数学归纳法
数学归纳法是一种论证方法,而递归是算法和程序设计的一种实现技术,数学归纳法是递归的基础。
递归算法设计的一般步骤
递归算法设计要先给出递归模型,再转换成对应的C/C++语言函数。
递归数据结构及其递归算法设计
1. 递归数据结构的定义
采用递归方式定义的数据结构称为递归数据结构。在递归数据结构定义中包含的递归运算称为基本递归运算。
2. 基于递归数据结构的递归算法设计
1) 单链表的递归算法设计
在设计不带头结点的单链表的递归算法时:
设求解以L为首结点指针的整个单链表的某功能为“大问题”。
而求解除首结点外余下结点构成的单链表(由L->next表示,而该运算为递归运算)的相同功能为“小问题”。
由大小问题之间的解关系得到递归体。
在考虑特殊情况,通常是单链表为空或者只有一个结点时,这是很容易求解,从而得到递归出口。
2)二叉树的递归算法设计
二叉树是一种典型的递归数据结构,当一颗二叉树采用二叉链b存储时:
设求解以b为根结点的整个二叉树的某功能为“大问题”。
求解其左、右子树的相同功能为“小问题”。
由大小问题之间的解关系得到递归体。
再考虑特殊情况,通常是二叉树为空或者只有一个结点时,这是很容易求解,从而得到递归出口。
递归算法设计示例