1、第六章 递 归6.1 递归的概念 6.2 基本递归过程 6.3 递归过程的实现与堆栈 6.4 用递归求解问题,定义:如果一个对象部分地包含它自己,或者利用自己定义自己的方式来定义或描述,则称这个对象是递归定义的(递归定义);如果一个过程直接或间接地调用自己,则称这个过程是一个递归过程(递归算法) 。 组成:递归部分、终止条件(递归出口),6.1 递归的概念,递归的例子xn=x*x*x*x (幂函数)P(1) = x 递归出口P(n)=P(n-1) * x, n1 递归部分S(n)=1+2+3+(n-1)+nS(1) = 1 递归出口S(n)=S(n-1)+n, n1 递归部分,以下三种情况适于
2、用递归求解问题: 问题的定义是递归的; 问题所涉及的数据结构是递归的; 问题的解法满足递归的性质。,1、问题的定义是递归的 阶乘函数、幂函数和斐波那契数列。 例1 阶乘函数的定义,求解阶乘函数的递归过程long Factorial(long n) if (n= =0)return 1; /递归终止条件elsereturn n * Factorial(n-1); /递归调用过程,例2 斐波那契数列Fib(n)的定义,求解斐波那契数列的递归算法long Fib ( long n ) if ( n = 1 ) return n; else return Fib (n-1) + Fib (n-2);
3、,2、问题所涉及的数据结构是递归的 例1 单链表节点类的递归定义,template class Node private:Node * next;public:T data; ,例2 单链表的递归定义head=NULL头指针为head的单链表的递归定义: (1)head指向一个空结点的数据结构是一个单链表 (2)head指向一个非空结点,该结点的指针域指向一个单链表,这样的数据结构是一个单链表。,头指针为p的单链表中搜索链表最后一个结点并打印其数值template void Find (Node *p) if ( p next = NULL )cout p data endl;else Fin
4、d ( p next ); ,3、问题的解法满足递归的性质 递归求解的基本思想(分治策略): 对于一个比较复杂的问题,如果能够把它分解为若干个相对简单的、而且解法相同或类似的子问题,那么当这些子问题获得解决时,原问题就获得解决。子问题无需分解就可以直接解决时,停止分解,直接求解该子问题递归出口。,例1 求文件中的最大元-最小元问题例2 汉诺塔(Tower of Hanoi)问题,归纳基础 递归出口 归纳步骤 递归调用,例3 数学归纳法证明:,因此,我们通常采用数学归纳法证明递归算法的正确性。而对于递归算法的时间复杂性分析,我们通常首先定义其时间复杂性的递归数学表达式,然后求解该递归公式。,6.
5、2 基本递归过程递归过程在实现时,发生递归调用分为:外部调用和内部调用。调用方式不同,返回的方式也不相同。,递归过程与实现的基本思想高级语言的编译程序中,函数调用是通过堆栈实现的; 递归函数调用是一种特殊的函数调用形式,因此也可以通过堆栈实现。,函数调用方式:,f1 f2 f3 fn,递归函数调用方式:,f(n) f(n-1) f(n-2) f(1),函数调用时执行入栈操作保存本次递归调用对应的信息 函数返回时执行出栈操作恢复上次递归调用对应的信息 一次递归调用所需要的信息表示为一个工作记录: 返回地址 参 数 (函数名、引用参数与实参等)局部变量,递归工作栈,栈顶工作记录对应当前递归调用过程
6、,称为当前活动记录。,long Factorial ( long n ) if ( n = 0 ) return 1;else return n * Factorial (n-1);RetLoc2void main ( ) int n;n = Factorial (4); RetLoc1,计算Factorial时活动记录的内容,F(3),F(2),F(1),F(0),递归算法的优点:递归过程结构清晰递归程序易写、易读正确性证明相对容易 递归算法的不足: 运行效率低,原因:函数调用空间开销大会出现重复计算,例 计算斐波那契数列Fib(n),递归算法long Fib ( long n ) if (
7、 n = 1 ) return n; else return Fib (n-1) + Fib (n-2); ,递归调用次数 SumCall(k) = SumCall(k-1)+SumCall(k-2)+1= O(2k),斐波那契数列的递归调用树,计算斐波那契数列的非递归函数 long CalFib(long n) if(n = 1) return n;else long f0 = 0,f1 = 1,f = 0;for( int i = 2;i = n;i+) f = f0+f1;f0 = f1;f1 = f;return f; 非递归算法的时间复杂性 O(n),6.3 递归过程的实现:堆栈与递
8、归递归过程实现的基本思想:采用堆栈模拟递归过程中子任务的产生和处理过程:产生新的子任务对应入栈操作;处理子任务对应弹栈操作;先处理的子任务后入栈;后处理的子任务先入栈。,CREATS ( S ):建立一个堆栈 S; S x : 元素 x 进栈;x S : 元素 x 出栈; StackEmpty(S): 若 S 为空,返回1.,例1 Hanoi塔问题,基本思想: 1. 借助C柱,从A柱将1至m-1号盘移至B柱; 2. 将A柱中剩下的第m号盘移至C柱; 3. 借助A柱,从B柱将1至m-1号盘移至C柱。 HANOI(m,A,B,C) HANOI(m-1,A,C,B) MOVE(A,C) HANOI(
9、m-1,B,A,C),算法 HR(m,i,j,k) / 把原柱i上的n个圆盘移到目标柱k上,圆柱j是中间柱 HR1递归出口IF m = 1 THEN (MOVE(i,k).RETURN). HR2递归调用HR(m-l,i,k,j) MOVE(i,k) HR(m-l,j,i,k) ,递归算法的实现堆栈 保存四元组 (m,i,j,k)HR(m-l,i,k,j) MOVE(i,k) HR(m-l,j,i,k)S(m-1,j,i,k).S(l,i,j,k) S(m-1,i,k,j),Hanoi塔的迭代算法,m 是原柱上圆盘的个数算法HI(m) HI1建立堆栈CREATS(S) HI2堆栈初始化S(m,
10、1,2,3) HI3利用栈实现递归WHILE NOT(StackEmpty(S)DO (n,i,j,k) SIF n = 1 THEN MOVE(i,k)ELSE(S(n-1,j,i,k).S(l,i,j,k)S(n-1,i,k,j) ,问题1:m个盘子的Hanoi塔问题,需移动多少次盘子?即HI的时间复杂性是多少?问题2:HI(m)需要多大的堆栈空间?即HI的空间复杂性是多少?,6.4 递归法求解问题,6.4.1 委员会问题 组合数学用于解决给定事件会以多少种方式发生的问题。 以经典的委员会问题为例,给出非负整数N和K,求从N个人中选出K个成员组成一个委员会的方法总数,即C(N, K)。,以
11、N6,K2为例对一般问题求解。 可以穷举出共有15种不同的选择方案。假设成员名分别为A、B、C、D、E和F。 当问题规模很大时,用分治策略将问题分解为较简单的子问题。简化问题的第一步是将A从小组中拿出,这样只剩下B、C、D、E和F。,子问题1:列出小组中剩下的5个人组成2人委员会的所有可能的组合情况。共有10种不同的子委员会,需要注意的是:每个新的委员会都不包含缺席者A。 子问题2:列出小组中剩下的5个人组成1人委员会的所有可能情况。显然,共有5种情况,即:(B),(C),(D),(E)和(F)。每个委员会要组成2人小组还缺1人。将成员A加入其中,它们即可变成两人委员会。这些两人委员会为:(A
12、,B),(A,C),(A,D),(A,E)和(A,F)。 可以断定子问题1和子问题2所得到的两人委员会包含了从原始问题得到的所有可能的委员会。,委员会问题的递归算法,算法COMM(n,k) COMM1递归出口IF k n THEN RETURN(0).ELSE IF(n = k OR k = 0) THEN RETURN(1). COMM2递归调用RETURN (COMM(n-1, k) + COMM(n-1, k-1),递归的应用回溯(backtracking) 寻找特定问题解的一种比较可靠的方法是首先列出所有候选解,然后依次检查每一个候选解,在检查完所有或部分候选解后,即可找到所需要的解。
13、 理论上,当候选解的数量有限并且通过检查所有或部分候选解能够得到所需要的解的时候,上述方法是可行的。对候选解进行系统检查的方法有多种,其中回溯和分枝限界法是比较常用的两种。,6.4.2 回 溯,回溯法试图通过建立部分的解决方法来得到整个问题的解决方案。该算法可将一个局部的解决方法扩展到整个问题。如果从局部的解决方法肯定不能得到整个问题的解决方案,也就是说局部的解将导致走进死胡同,这时就要通过移除最近加入的部分将算法回退,并且尝试其他的方法,回溯的基本思想是:为了求得问题的解,先选择某一种可能情况向前探索,在探索过程中,一旦发现原来的选择是错误的,就退回一步重新选择,继续向前探索,如此反复进行,
14、直至得到解或证明无解。 回溯法解决的问题:货船装箱、背包、最大完备子图、旅行商和电路板排列,例: n-皇后问题 在国际象棋中,最强大的棋子是皇后,因为她能攻击她所在行、所在列内或沿对角方向的任何一个棋子。n-皇后问题要求在棋盘上放置n个皇后,使得没有哪个皇后能攻击其他的皇后。,在回溯中,由于每个皇后都必须放置在不同的行中,所以n-皇后问题的解决方案可以表示为一个n元组(x1, , xn),这里的xi 是把第i个皇后放在第i行的列数且1 xi n. 由于任何两个皇后都不能出现在同一列中,因此n元组(x1, , xn)中没有两个元素是相同的。 那么,应该如何判断两个皇后是否在同一斜角线上呢?,如果
15、设想棋盘的方格像二维数组A(1: n, 1: n)的下标那样标记,对于在同一条斜角线上的由左上方到右下方的每一个元素都有相同的“行列”值,同样,在同一条上的由右上方到左下方的每一个元素则有相同的“行+列”值。假设有两个皇后被放置在(i, j)和(k, l)位置上,那么根据以上所述,仅当ij kl 或 i+j k+l 时,它们才在同一条斜角线上。将这两个等式分别变换成jl ik 或 jl ki 因此,当且仅当 | jl | | ik | 时,两个皇后在同一条斜角线上,这里的| jl |为jl的绝对值。,n-皇后问题的解为一个n元组,使用一个大小为n的数组queenInRow来存储。其中queen
16、InRow k表示第k行的第k个皇后所在列的位置。例如queenInRow 1=7表示第一个皇后被放在第一行、第7列。假设已经将第k-1个皇后放入第k-1行中,接下来尝试将第k个皇后放入第k行的某一列。编写算法PLACE(k, i . result),当第k个皇后能放置于第k行第i列则返回true;否则返回false。,算法PLACE要测试两种情况,即queenInRow k是否不同于前面queenInRow 1,queenInRowk1的值以及在同一条斜角线上是否有别的皇后。,算法PLACE( k, i . result) /*如果一个皇后能放在第k行第i列则返回true;否则返回false
17、,结果保存在result中 */ PLACE1 初始化j1.resulttrue. PLACE2 判定能否放置,WHILE j k DO( IF queenInRow (j) = i /*列i中已经放置皇后*/OR ABS(queenInRow ji) = ABS(jk) /*斜角线上已经放皇后了*/THEN (resultfalse. RETURN. )jj+1.) ,算法NQUEENS( k) /*此算法使用递归算法求出在一个nn的棋盘上放置n个皇后,使其不能互相攻击的所有可能位置 */FOR i = 1 TO n DO( PLACE(k,i. canPlace). /*此处能放这个皇后吗*/IF canPlace = true THEN /*能放置*/( queenInRowki. IF k= n THEN PRINT(queenInRow). /*找到一个解,打印出来*/ELSE NQUEENS(k+1). ) ) /*考虑下一个皇后*/,