520 likes | 651 Views
第四章 非递归化. 递归技术是经常使用的一种程序设计方法。在树的遍历、排序等多个问题的程序编制中曾使用过递归技术。 递归技术使程序简洁、明了、 易懂。 同时我们也知道,递归程序运行需要的时间和空间都比非递归程序多。. 讨论如何用非递归程序解决递归定义的问题。 此为“非递归化”问题。讨论非递归化问题的另一个原因在于 : 汇编语言及有些高级语言 ( 如 FORTRAN) 不允许子程序被递归调用。. 递归有直接递归和间接递归之分。 函数 P 的函数体中有调用 P 的语句,这称为直接递归; 如函数 P 调用函数 Q ,函数 Q 又调用函数 P ,这称为间接递归。
E N D
递归技术是经常使用的一种程序设计方法。在树的遍历、排序等多个问题的程序编制中曾使用过递归技术。递归技术是经常使用的一种程序设计方法。在树的遍历、排序等多个问题的程序编制中曾使用过递归技术。 • 递归技术使程序简洁、明了、 易懂。 • 同时我们也知道,递归程序运行需要的时间和空间都比非递归程序多。
讨论如何用非递归程序解决递归定义的问题。 • 此为“非递归化”问题。讨论非递归化问题的另一个原因在于: • 汇编语言及有些高级语言(如FORTRAN) 不允许子程序被递归调用。
递归有直接递归和间接递归之分。 • 函数P的函数体中有调用P的语句,这称为直接递归; • 如函数P调用函数Q,函数Q又调用函数P,这称为间接递归。 • 本章主要讨论直接递归问题的非递归化问题
在编写程序时,程序的结构应与要解决的问题的逻辑结构尽量接近。在编写程序时,程序的结构应与要解决的问题的逻辑结构尽量接近。 • 例如“顺序搜寻”问题:给定一个一维数组LIST及定值X,问LLST中哪一个元素等于X.“顺序搜寻”的递归描述分为两个步骤: • 1.检测数组LIST中的一个元素(可为最末尾的元素)是否与X相等,若不等则 • 2.“顺序搜寻”数组LIST的其余元素组成的数组。
递归是通用的技术,可用于描述所有的循环问题。下述循环语句递归是通用的技术,可用于描述所有的循环问题。下述循环语句 • while (C) S • 可改写为对如下函数的调用语句 • void P( ) • { if( C) • { S • P( ); • } • }
反之亦然,递归也可改写为循环语句。 • 以前述的“顺序搜寻”问题为例。若输出为:与X有相同值的元素的下标。 • 共有三种情况要考虑: • 1.数组LIST的元素都已经被检测(不成功) • 2.数组最末尾的元素与X相等 • 3.须“顺序搜寻”数组的其余元素
顺序搜寻的递归函数(从尾部开始搜索): • int SEQUENTIAL(N, X) • //在数组LIST [0 · · N]中搜寻X • {IF (N<0) return -1; • EISE IF (X= =L1ST [N]) return N; • EISE SEQUENTIAL(N一1, X); • } • 这个函数很容易改写为非递归的过程。 • 甚至这个问题本身就不需要递归技术.
§4-1 递归问题 考虑几个有代表意义的递归问题
“二元折半搜寻”。这种搜寻技术要求数组的元素事先排序。使用递归技术很容易写出二元搜寻的递归函数:“二元折半搜寻”。这种搜寻技术要求数组的元素事先排序。使用递归技术很容易写出二元搜寻的递归函数: • int BSEARCH (X, L, R) • //在数组L1ST[L · · R]中搜寻X • {iF(L>R) return -1; • else {M = (L+R)/ 2; • IF(X= =LIST [M]) return M; • ELSE IF(X>L1ST[M]) • BSEARCH (X, M+l,R); • else BSEARCH (X, L, M—l) • } • }
其中M为局部变量。由函数的结构可以看出二元搜寻包含了四种可能情况。其中M为局部变量。由函数的结构可以看出二元搜寻包含了四种可能情况。 • 1.无法对数组搜寻 • 2.X已经发现 • 3.应搜寻数组的左半部 • 4.应搜寻数组的右半部
“快速排序”法有如下递归过程 • void QUICK (L, R); • //将数组L1ST [L · · R]排序 • {int P; • IF (L<R) • { P=PARTITION (L, R); • QUICK (L, P - 1); • QUlCK(P+1,R) • } • }
其中过程PARTITION的功能为确定某一个元素的最终位置,并将所有不大于它的元素放其中过程PARTITION的功能为确定某一个元素的最终位置,并将所有不大于它的元素放 • 置在其左侧,将所有大于它的元素都放置在其右侧。 • 因此,函数Quick的思路就很容易理解了, • 先调用PARTITION将数组的所有元素按某一元素的值划成两组,分别置于该元素 • 的左、右两侧。从而,剩余的工作就是将这两组元素各自排序: • Quick(P+1,R)对右侧的元素排序,而Quick(L,P—1)对左侧的元素排序。
函数PARTITION以数组L1ST最 • 左边的元素LIST[L]区分L1ST的所有元素,且LlST [L]最终占据数组LlST的第P个位置。
int PARTITION (L, R); • { int J=L+1; P=R; • do{ • while ((L1ST [j] <=LIST [L])&&(j<P)) J++; • WHILE (L1ST [P] >LIST [L]) P- -; • if (P>J) • { SWAP (J, P); • J++; P- -; • } //if • } while( P >J); • SWAP (L, P) ; return P; • } SWAP(L,P) 交换L1ST[L]和LIST[P]的位置。
河内塔问题的算法可以改写为下述函数 • void TOWER (N, A, B, C); • //将N个盘从A杆移到C杆 • {IF (N>0) • {TOWER(N一1,A,C,B); • WRITELN(N,A,C); • //打印信息:将N号盘从A杆移到c杆 • TOWER(N一1,B,A,C); • } • }
排列问题:给定由M个不同的字符组成的一维数组LIST[1 · · M], • 输出由这M个字符组成的所有可能的排列。 • 假设我们有办法写出M一1个字符的所有可能排列,则问题的要求很容易实现。
固定LIST [M]不动, • 产生前M一1个字符的所有可能排列; • 然后将LIST[M]与前M一1个字符中的某一个交换位置, • 固定新的LIST[M]不动,产生现在的前M—1个字符所有可能的排列。 • 依次让这M个字符的每一个轮流固定在LIST的末尾,产生其它M一1个字符所有可能的排 • 列。
为不重复、无遗漏地让每个字符都能固定在LIST的末尾,在每次为LIST[M]选用新为不重复、无遗漏地让每个字符都能固定在LIST的末尾,在每次为LIST[M]选用新 • 的字符之前,要恢复其原先的字符。
排列问题的递归函数为PERMU(M),因此PERMU(M - 1)表示LIST[M]已固定, • 现要固定LIST的第M - 1个元素,依次类推,直至PERMU(1)表示LIST的第M个、第 • M - 1个、…、第2个元素都已固定,现在要固定LIST的第1个元素;显然,此时无需选择, • LIST[1]已经固定了,即我们已经得到了一个排列,只要将之打印出来即可。以下 • 的递归算法就很容易理解了
void PERMU(M) • { • IF (M= =1) PRINT LIST; • ELSE (FOR K=M;K>= l;K--) • {SWAP(K,M); • PERMU(M - 1); • SWAP(K,M) • } • }
注意,每一个递归调用都是条件执行的,只有这样才能防止无限循环注意,每一个递归调用都是条件执行的,只有这样才能防止无限循环 • 此为递归出口. 否则, 没有出口, 运行就陷入无限循环
§4.2 栈 • 递归就其本质而言,是算法的循环执行,因此可改写为非递归的函数。大致讲, • 过程中要有一个循环结构,这个循环结构包含整个递归算法。原先的递归调用语句则改写为
1.参量的值的重新赋值,然后 • 2.转入过程的某一部分,继续执行
考虑折半搜寻问题。上节的递归算法BSEARCH中两次递归调用BSEARCH.考虑折半搜寻问题。上节的递归算法BSEARCH中两次递归调用BSEARCH. • 在非递归算法中,需要局部变量LEFT和RIGHT.第一次递归调用 • BSEARCH(X,M+1,R) • 改写为 • LEFT=M+1 • 再转入循环体的开始(因为循环体无条件执行)。第二次递归调用 • BSEARCH(X,L,M - 1) • 改写为 • RIGHT=M - l • 再转入循环体的开始。从而,BSEARCH的非递归过程BSEARCHl如下所示。
Int BSEARCH_l (X,L,R) • { int LEFT =L ,RIGHT =R; • do{ • IF (L>R) return -1; • ELSE { • M=(LEFT+RIGHT) / 2; • IF(X= =LIST[M]) return M; • ELSE IF(X>LIST[M]) LEFT=M+1; • ELSE RIGHT=M – 1; • } • } while (1); • }
过程BSEARCH_l与原来的递归过程BSEARCH结构上很相似。do-while循环语过程BSEARCH_l与原来的递归过程BSEARCH结构上很相似。do-while循环语 • 句包含了整个递归函数的函数体, • 其中加入了两个return,这两个return出现在不含 • 有递归调用的条件选择部份的末尾; • 递归调用语句改写成参量的重新赋值。
这种直观的方法可用来改写一类递归过程, • 此类过程的特征为:每一条件选择程序 • 段至多含有一个递归调用语句, • 且该递归调用语句是过程的最后一个语句。这种递归称为尾部递归, • 尾部递归的非递归化可按下述步骤进行
1.把递归算法整个的过程体做成循环语句的循环体1.把递归算法整个的过程体做成循环语句的循环体 • 2.在不包含递归调用的条件分枝末尾加EXIT语句 • 3.将递归调用语句替换成其参量的重新赋值,再转入过程的某一点
但是大多数递归问题不属于尾部递归(如快速排序、河内塔问题及排列问题)。但是大多数递归问题不属于尾部递归(如快速排序、河内塔问题及排列问题)。 • 有些条件分枝含有多个递归调用;有些递归调用语句之后、该条件分枝结束以前还有操作要执行。 • 这就需要将一些参量的值、局部变量的值恢复为递归调用语句之前的值, • 以使前述的一些工作、语句得以正确执行。
例如河内塔问题的递归函数TOWER中,两次递归调用TOWER.在第一次调用例如河内塔问题的递归函数TOWER中,两次递归调用TOWER.在第一次调用 • TOWER(其第一个实参值为N - 1)的执行过程中,很可能再次调用TOWER,参量N的 • 值会变化。但是毫无疑问,在执行打印语句 • WRITELN(N,A,C) • 时,要求N的值为第一次调用TOWER之前的值。在执行第二次递归调用 • TOWER(N - 1,B,A,C) • 时也有同样的要求。
又例如递归函数PERMU中,递归调用PERMU,从而, K的值会因为PERMU • 再次调用而发生变化,但在递归调用PERMU之后的语句 • SWAP(K,M) • 执行时,K所需要的值是递归调用PERMU之前K所具有的值。
由此,非尾部递归类型问题的关键在于:如果一个变量的值在递归调用结束后还会由此,非尾部递归类型问题的关键在于:如果一个变量的值在递归调用结束后还会 • 用到,则在该递归调用执行之前,必须保存这个值;在该递归调用结束时,必须将这个 • 值恢复,即将保存的值恢复。C++等语言有自动保存、恢复措施。
过程BSEARCH的变量值无需保护, • 这是因为递归调用后没有任何工作、语句要执行
递归调用的执行,一般会导致在该次调用结束前,此函数再次调用一次或数次。递归调用的执行,一般会导致在该次调用结束前,此函数再次调用一次或数次。 • 在此逐次调用的过程中,后保存的值一定比先保存的值先恢复, • 换言之,先保存的值一定比后保存的值后恢复。 • 显然递归过程的非递归化所需要的数据结构为“栈”。
递归过程中每一个需要保存的变量都要设置一个栈,因此可能有多个栈,递归过程中每一个需要保存的变量都要设置一个栈,因此可能有多个栈, • 但是所有这些栈用同一个指针。 • 除了变量、参量需要栈以外, • 还要设置一个返回栈,用于记载递归调用运行结束后,应回何处继续执行的信息(即记载返回点的信息) • 这里所说的返回, 和C++的return不同.
递归过程的开始点、结束点和返回点 • 统称关键点。递归函数TOWER有四个关键点。
河内塔问题的函数 • void TOWER (N, A, B, C) • //将N个盘从A杆移到C杆 • {① IF (N>0) • {TOWER(N - 1,A,C,B); ② • WRITELN(N,A,C); • //打印信息:将N号盘从A杆移到c杆 • TOWER(N - 1,B,A,C); ③ • } • ④}
很明显,最后两个关键点可以合并, • 因为第二次递归调用执行完毕,整个过程就执行结束了。
非递归化,就是要把函数改写为等价的但是没有递归调用的函数非递归化,就是要把函数改写为等价的但是没有递归调用的函数 • 具体到函数TOWER,就是要把函数体中的两个TOWER用等价的语句来替代 • 在第一个关键点处要做的工作为: 将栈清零,SP=0 给诸局部变量赋值
在最后一个关键点处要做的工作为: 调用过程POP,恢复保存在栈中的值 • 将递归算法改建为一个循环语句。 • 循环语句结束的条件为“栈空”。
具体到函数TOWER,就是要把函数体中的两个TOWER用等价的语句来替代具体到函数TOWER,就是要把函数体中的两个TOWER用等价的语句来替代 • 要保存此时变量的值,保存返回的地点;并且给局部变量赋值
函数TOWER_l调用三个过程: • SWAP(X,Y)的功能是交换X和Y的值, • POP的功能是恢复保护在栈中的值, • PUSH的功能是将值保存到栈去。
Void PUSH(int R) • { • IF(SP= =STACKSIZE) exit(OVERFLOW); • ELSE {SP++; • NUM_STACK [SP]=NUM; • X_STACK[SP]=X; • Y_STACK[SP]=Y; • Z_STACK[SP]=Z; , • CP_STACK [SP]=R • } • // R的值指示着返回点} }
Void POP( ) • {NUM=NUM_STACK[SP]; • X:=X_STACK[SP]; • Y:=Y_STACK [SP]; • Z:=Z_STACK[SP]; • CP=CP_STACK [SP]; • SP--; • }
void TOWER_l (int N,char A,B,C) {#define STACKSIZE 40 //栈的最大高度 • int sp=0, //清栈 • num=N,x=A,y=B,z=C;//局部变量赋值 • cp=1; //分枝点 • INT_STACK num_stack, x_stack, y_stack, • z_stack,cp_stack; //栈型数据结构 • do switch(cp) • {‥‥‥} //见下页 • while (sp>0); }
CASE 1: • IF (NUM>0) • { PUSH (2); • NUM--; • SWAP (Y,Z); • CP=1 //转向循环的开始处 • } • else CP=4; //转向调用过程POP • break;
Case 2: PRlNT(NUM, X, Z); • PUSH (3); • NUM--; • SWAP(X,Y); • CP=l;break; {转向循环的开始处} • Case 3: CP:=4; {转向调用过程POP} • break; • Case 4: POP; break;
非递归化所应遵循的步骤如下: • 1. 设置所需的局部变量及分枝控制变量CP; • 2. 确定递归过程中的各关键点; • 3. 过程开始处给局部变量赋值并清栈; • 4. 将整个递归过程的过程体包含在循环语句中,它包含多个条件分枝,由CP的值确定哪个分枝应被执行; • 5. 将各关键点之间的语句放入不同的选择分枝中;
6.在不含递归调用的分枝末尾调用过程POP; • 7.将尾部递归替换成其参量的赋值语句,并转移至第一个关键点; • 8.对非尾部递归语句,要将参变量的值及返回点进栈,再转移至第一个关键点; • 9.在每个分枝的末尾给分枝控制变量CP赋值,以指明执行的转向; • 10.递归算法的结束处,调用过程POP; • 11.循环语句的结束条件为栈空。