第六章 递 归\r\r6.1 递归的概念\r6.2 基本递归过程\r6.3 递归过程的实现与堆栈\r
定义:如果一个对象部分地包含它自己,或者利用自己定义自己的方式来定义或描述,则称这个对象是递归的(递归定义);如果一个过程直接或间接地调用自己,则称这个过程是一个递归过程(递归算法) 。\r组成:递归部分、终止条件(递归出口)
6.1 递归的概念
递归的例子\r\r xn=x*x*…*x*x (幂函数)\r P(1) = x 递归出口\r P(n)=P(n-1) * x, n>1 递归部分\r S(n)=1 2 3 … (n-1) n\r S(1) = 1 递归出口\r S(n)=S(n-1) n, n>1 递归部分\r
以下三种情况适于用递归求解问题:\r问题的定义是递归的;\r问题所涉及的数据结构是递归的;\r问题的解法满足递归的性质。
1、问题的定义是递归的\r阶乘函数、幂函数和斐波那契数列。\r[例1] 阶乘函数的定义\r
求解阶乘函数的递归过程\r\rlong Factorial(long n)\r{\r if (n= =0)\r return 1; //递归终止条件\r else\r return n * Factorial(n-1);\r} //递归调用过程
[例2] 斐波那契数列Fib(n)的定义
求解斐波那契数列的递归算法\r\rlong Fib ( long n ) { if ( n <= 1 ) return n; else return Fib (n-1) Fib (n-2);\r}
2、问题所涉及的数据结构是递归的\r[例1] 单链表节点类的递归定义\r \r\r
template < class T > class Node \r{\r private:\r Node < T > * next;\r public:\r T data;\r … …\r}
[例2] 单链表的递归定义\r head=NULL\r\r\r头指针为head的单链表的递归定义:\r(1)head指向一个空结点的数据结构是一个单链表\r(2)head指向一个非空结点,该结点的指针域指向一个单链表,这样的数据结构是一个单链表。
头指针为p的单链表中搜索链表最后一个结点并打印其数值\r\rtemplate
3、问题的解法满足递归的性质\r递归求解的基本思想(分治策略): \r 对于一个比较复杂的问题,如果能够把它分解为若干个相对简单的、而且解法相同或类似的子问题,那么当这些子问题获得解决时,原问题就获得解决。\r 子问题无需分解就可以直接解决时,停止分解,直接求解该子问题--递归出口。
[例1] 求文件中的最大元-最小元问题\r\r[例2] 汉诺塔(Tower of Hanoi)问题\r\r \r
归纳基础 ? 递归出口 \r 归纳步骤 ? 递归调用\r
[例3] 数学归纳法证明:
因此,我们通常采用数学归纳法证明递归算法的正确性。\r 而对于递归算法的时间复杂性分析,我们通常首先定义其时间复杂性的递归数学表达式,然后求解该递归公式。
6.2 基本递归过程\r \r 递归过程在实现时,发生递归调用分为:\r 外部调用和内部调用。\r 调用方式不同,返回的方式也不相同。\r
递归过程与实现的基本思想\r\r高级语言的编译程序中,函数调用是通过堆栈实现的;\r递归函数调用是一种特殊的函数调用形式,因此也可以通过堆栈实现。
函数调用方式:
f1 f2 f3 … … fn
递归函数调用方式:
f(n) f(n-1) f(n-2) … … f(1)
函数调用时执行入栈操作保存本次递归调用对应的信息\r函数返回时执行出栈操作恢复上次递归调用对应的信息\r一次递归调用所需要的信息表示为一个工作记录: \r 返回地址 \r 参 数 (函数名、引用参数与实参等)\r 局部变量
递归工作栈
栈顶工作记录对应当前递归调用过程,称为当前活动记录。
long Factorial ( long n ) {\r if ( n == 0 ) return 1;\r else return n * Factorial (n-1);\r RetLoc2\r }\r \r void main ( ) {\r int n;\r n = Factorial (4);\rRetLoc1\r }
计算Factorial时活动记录的内容
F(3)
F(2)
F(1)
F(0)
递归算法的优点:\r 递归过程结构清晰\r 递归程序易写、易读\r 正确性证明相对容易\r递归算法的不足:\r运行效率低,原因:\r 函数调用空间开销大\r 会出现重复计算
[例] 计算斐波那契数列Fib(n)
递归算法\r\rlong Fib ( long n ) { if ( n <= 1 ) return n; else return Fib (n-1) Fib (n-2);\r}
递归调用次数 \rSumCall(k) = SumCall(k-1) SumCall(k-2) 1\r = O(2k)\r
斐波那契数列的递归调用树
计算斐波那契数列的非递归函数\rlong CalFib(long n)\r{ \r if(n < = 1) \r return n;\r else \r {\r long f0 = 0,f1 = 1,f = 0;\r for( int i = 2;i < = n;i )\r { f = f0 f1;f0 = f1;f1 = f;}\r return f;\r }\r} \r非递归算法的时间复杂性 O(n) \r
当k=35时,斐波那契数迭代函数需进行33次加法,而递归函数需要进行185万次函数调用!\r斐波那契数的例子对于使用递归方案可能带来的问题是个很好的警示。由于函数调用产生的额外开销,一个简单的递归函数也有可能严重地损害程序的运行性能。更严重的结果是,一次递归调用可能产生一层接一层的递归嵌套调用,程序对堆栈的需求也会超出可用栈空间的范围,以至于整个程序超出程序员的控制。\r斐波那契数的例子是一个极端的情况。实际上,斐波那契数递归算法的低效率与该算法内在效率低也有很大关系。有另外一些递归算法,如折半查找(该算法也有非递归的迭代形式),效率比较高。
因此,递归仍然是一种十分重要的算法设计方法。\r许多算法使用递归方式更便于叙述和设计,可以很自然的用递归结束条件和递归步骤实现递归。关于何时使用递归没有硬性规定,必须权衡一下设计和运行时的复杂度。当强调算法设计简洁性和易读性,而且在运行时有合理的时空复杂度时可采用递归方法。
8.3 递归过程的实现:堆栈与递归\r \r递归过程实现的基本思想:\r 采用堆栈模拟递归过程中子任务的产生和处理过程:\r 产生新的子任务对应入栈操作;\r 处理子任务对应弹栈操作;\r 先处理的子任务后入栈;\r 后处理的子任务先入栈。
CREATS ( S ):建立一个堆栈 S;\rS ? x : 元素 x 进栈;\r x ? S : 元素 x 出栈;\rStackEmpty(S): 若 S 为空,返回1.
例1 Hanoi塔问题
基本思想:\r1. 借助C柱,从A柱将1至m-1号盘移至B柱;\r2. 将A柱中剩下的第m号盘移至C柱;\r3. 借助A柱,从B柱将1至m-1号盘移至C柱。 \r\rHANOI(m,A,B,C) ?HANOI(m-1,A,C,B) \r MOVE(A,C) . HANOI(m-1,B,A,C)
算法 HR(m,i,j,k) \r// 把原柱i上的n个圆盘移到目标柱k上,圆柱j是中间柱\rHR1[递归出口]\r IF m = 1 THEN (MOVE(i,k).RETURN). \rHR2[递归调用]\r HR(m-l,i,k,j) .\r MOVE(i,k) .\r HR(m-l,j,i,k) ▌
递归算法的实现\r\r堆栈 保存四元组 (m,i,j,k)\r HR(m-l,i,k,j) .\r MOVE(i,k) .\r HR(m-l,j,i,k)\r S?(m-1,j,i,k).\r S?(l,i,j,k) .\r S?(m-1,i,k,j)
Hanoi塔的迭代算法,m 是原柱上圆盘的个数\r\r算法HI(m)\rHI1[建立堆栈]\r CREATS(S). \rHI2[堆栈初始化]\r S?(m,1,2,3). \rHI3[利用栈实现递归]\r WHILE NOT(StackEmpty(S))DO \r ((n,i,j,k) ?S.\r IF n = 1 THEN MOVE(i,k)\r ELSE(S?(n-1,j,i,k).\r S?(l,i,j,k).\r S?(n-1,i,k,j)) ▌
问题1:m个盘子的Hanoi塔问题,需移动多少次盘子?\r 即HI的时间复杂性是多少?\r\r问题2:HI(m)需要多大的堆栈空间?\r 即HI的空间复杂性是多少?\r