第二章 递归算法设计技术
1. 什么是递归
1.1 递归的定义
在定义一个过程或函数时出现调用本过程或本函数的成分,称之为递归。
若调用自身,称之为直接递归;若过程或函数p调用过程或函数q,而q又调用p,称之为间接递归。任何间接递归都可以等价地转换为直接递归。
如果一个递归过程或递归函数中递归调用语句是最后一条执行语句,则称这种递归调用为尾递归。
-
一般来说,能够用递归解决的问题应该满足以下三个条件:
- 需要解决的问题可以转化为一个或多个子问题来求解,而这些子问题的求解方法与原问题完全相同,只是在数量规模上不同
- 递归调用的次数必须是有限的
- 必须有结束递归的条件来终止递归
-
示例:设计求
n!
(n为正整数)的递归算法int fun(int n){ if(n == 1){ return 1; }else{ return fun(n-1) * n; } } // 直接递归函数 // 尾递归函数
1.2 何时使用递归
-
定义是递归的:有许多数学公式、数列等的定义是递归的。例如,求
n!
和Fibonacci数列
等。这些问题的求解过程可以将其递归定义直接转化为对应的递归算法 -
数据结构是递归的
-
有些数据结构是递归的,例如:单链表就是一种递归数据结构,其结点类型声明如下:
typedef struct LNode{ ElemType data; struct LNode *next; }LinkList;
-
对于递归数据类型,采用递归的方法编写算法既方便又有效,例如:求一个不带头结点的单链表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->lchild) + bt->data; } }
-
-
问题的求解方法是递归的:有些问题的解法是递归的,典型的有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); Hanoi(n-1, y, x, z);
1.3 递归模型
递归模型是递归算法的抽象,它反映一个递归问题的递归结构
例如前面的递归算法对应的递归模型如下:
fun(1) = 1 (1) fun(n) = n * fun(n-1) n>1 (2)
其中,第一个式子给出了递归的终止条件,第二个式子给出了
fun(n)
的值与fun(n-1)
的值之间的关系,我们把第一个式子称为递归出口,把第二个式子称为递归体
一般地,一个递归模型由递归出口和递归体两部分组成,前者确定递归到何时结束,后者确定递归求解时的递推关系
递归出口的一般格式:f(s1) = m1
(这里的s1与m1均为常量,有些递归问题可能有多个递归出口)
递归体的一般格式:f(s(n+1)) = g(f(si), f(s(i+1)), ..., f(sn), cj, c(j+1), ..., cm)
(其中,n、i、j、m均为正整数;这里的s(n+1)是一个递归“大问题”,si、s(i+1)、…、sn是递归“小问题”;cj、c(j+1)、 …、cm是若干个可以直接(用非递归方法)解决的问题,g是一个非递归函数,可以直接求值)
1.4 递归算法的执行过程
-
一个正确的递归程序虽然每次调用的是相同的子程序,但它的参量、输入数据等均有变化
-
在正常的情况下,随着调用的不断深入,必定会出现调用到某一层的函数时,不再执行递归调用而终止函数的执行,遇到递归出口便是这种情况
-
递归调用是函数嵌套调用的一种特殊情况,即它是调用自身代码,也可以把每一次递归调用理解成调用自身代码的一个复制件
-
由于每次调用时,它的参量和局部变量均不相同,因而也就保证了各个复制件执行时的独立性
-
系统为每一次调用开辟一组存储单元,用来存放本次调用的返回地址以及被中断的函数的参量值
-
这些单元以系统栈的形式存放,每调用一次进栈一次,当返回时执行出栈操作,把当前栈顶保留的值送回相应的参量中进行恢复,并按栈顶中的返回地址,从断点继续执行
-
示例1:上述求
n!
的例子,求fun(5)
时内部栈的变化及求解过程从以上过程可以得出:每递归调用一次,就需进栈一次,最多的进栈元素个数称为递归深度,当n越大,递归深度越深,开辟的栈空间也越大;每当遇到递归出口或完成本次执行时,需退栈一次,并恢复参量值,当全部执行完毕时,栈应为空
归纳起来,递归调用的实现是分两步进行的:第一步是分解过程,即用递归体将“大问题”分解成“小问题”,直到递归出口为止;然后进行第二步的求值过程,即已知“小问题”,计算“大问题”
-
示例2:Fibonacci数列
int Fib(int n