1 / 52

第四章 非递归化

第四章 非递归化. 递归技术是经常使用的一种程序设计方法。在树的遍历、排序等多个问题的程序编制中曾使用过递归技术。 递归技术使程序简洁、明了、 易懂。 同时我们也知道,递归程序运行需要的时间和空间都比非递归程序多。. 讨论如何用非递归程序解决递归定义的问题。 此为“非递归化”问题。讨论非递归化问题的另一个原因在于 : 汇编语言及有些高级语言 ( 如 FORTRAN) 不允许子程序被递归调用。. 递归有直接递归和间接递归之分。 函数 P 的函数体中有调用 P 的语句,这称为直接递归; 如函数 P 调用函数 Q ,函数 Q 又调用函数 P ,这称为间接递归。

sanne
Download Presentation

第四章 非递归化

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. 第四章 非递归化

  2. 递归技术是经常使用的一种程序设计方法。在树的遍历、排序等多个问题的程序编制中曾使用过递归技术。递归技术是经常使用的一种程序设计方法。在树的遍历、排序等多个问题的程序编制中曾使用过递归技术。 • 递归技术使程序简洁、明了、 易懂。 • 同时我们也知道,递归程序运行需要的时间和空间都比非递归程序多。

  3. 讨论如何用非递归程序解决递归定义的问题。 • 此为“非递归化”问题。讨论非递归化问题的另一个原因在于: • 汇编语言及有些高级语言(如FORTRAN) 不允许子程序被递归调用。

  4. 递归有直接递归和间接递归之分。 • 函数P的函数体中有调用P的语句,这称为直接递归; • 如函数P调用函数Q,函数Q又调用函数P,这称为间接递归。 • 本章主要讨论直接递归问题的非递归化问题

  5. 在编写程序时,程序的结构应与要解决的问题的逻辑结构尽量接近。在编写程序时,程序的结构应与要解决的问题的逻辑结构尽量接近。 • 例如“顺序搜寻”问题:给定一个一维数组LIST及定值X,问LLST中哪一个元素等于X.“顺序搜寻”的递归描述分为两个步骤: • 1.检测数组LIST中的一个元素(可为最末尾的元素)是否与X相等,若不等则 • 2.“顺序搜寻”数组LIST的其余元素组成的数组。

  6. 递归是通用的技术,可用于描述所有的循环问题。下述循环语句递归是通用的技术,可用于描述所有的循环问题。下述循环语句 • while (C) S • 可改写为对如下函数的调用语句 • void P( ) • { if( C) • { S • P( ); • } • }

  7. 反之亦然,递归也可改写为循环语句。 • 以前述的“顺序搜寻”问题为例。若输出为:与X有相同值的元素的下标。 • 共有三种情况要考虑: • 1.数组LIST的元素都已经被检测(不成功) • 2.数组最末尾的元素与X相等 • 3.须“顺序搜寻”数组的其余元素

  8. 顺序搜寻的递归函数(从尾部开始搜索): • 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); • } • 这个函数很容易改写为非递归的过程。 • 甚至这个问题本身就不需要递归技术.

  9. §4-1 递归问题 考虑几个有代表意义的递归问题

  10. “二元折半搜寻”。这种搜寻技术要求数组的元素事先排序。使用递归技术很容易写出二元搜寻的递归函数:“二元折半搜寻”。这种搜寻技术要求数组的元素事先排序。使用递归技术很容易写出二元搜寻的递归函数: • 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) • } • }

  11. 其中M为局部变量。由函数的结构可以看出二元搜寻包含了四种可能情况。其中M为局部变量。由函数的结构可以看出二元搜寻包含了四种可能情况。 • 1.无法对数组搜寻 • 2.X已经发现 • 3.应搜寻数组的左半部 • 4.应搜寻数组的右半部

  12. “快速排序”法有如下递归过程 • void QUICK (L, R); • //将数组L1ST [L · · R]排序 • {int P; • IF (L<R) • { P=PARTITION (L, R); • QUICK (L, P - 1); • QUlCK(P+1,R) • } • }

  13. 其中过程PARTITION的功能为确定某一个元素的最终位置,并将所有不大于它的元素放其中过程PARTITION的功能为确定某一个元素的最终位置,并将所有不大于它的元素放 • 置在其左侧,将所有大于它的元素都放置在其右侧。 • 因此,函数Quick的思路就很容易理解了, • 先调用PARTITION将数组的所有元素按某一元素的值划成两组,分别置于该元素 • 的左、右两侧。从而,剩余的工作就是将这两组元素各自排序: • Quick(P+1,R)对右侧的元素排序,而Quick(L,P—1)对左侧的元素排序。

  14. 函数PARTITION以数组L1ST最 • 左边的元素LIST[L]区分L1ST的所有元素,且LlST [L]最终占据数组LlST的第P个位置。

  15. 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]的位置。

  16. 河内塔问题的算法可以改写为下述函数 • 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); • } • }

  17. 排列问题:给定由M个不同的字符组成的一维数组LIST[1 · · M], • 输出由这M个字符组成的所有可能的排列。 • 假设我们有办法写出M一1个字符的所有可能排列,则问题的要求很容易实现。

  18. 固定LIST [M]不动, • 产生前M一1个字符的所有可能排列; • 然后将LIST[M]与前M一1个字符中的某一个交换位置, • 固定新的LIST[M]不动,产生现在的前M—1个字符所有可能的排列。 • 依次让这M个字符的每一个轮流固定在LIST的末尾,产生其它M一1个字符所有可能的排 • 列。

  19. 为不重复、无遗漏地让每个字符都能固定在LIST的末尾,在每次为LIST[M]选用新为不重复、无遗漏地让每个字符都能固定在LIST的末尾,在每次为LIST[M]选用新 • 的字符之前,要恢复其原先的字符。

  20. 排列问题的递归函数为PERMU(M),因此PERMU(M - 1)表示LIST[M]已固定, • 现要固定LIST的第M - 1个元素,依次类推,直至PERMU(1)表示LIST的第M个、第 • M - 1个、…、第2个元素都已固定,现在要固定LIST的第1个元素;显然,此时无需选择, • LIST[1]已经固定了,即我们已经得到了一个排列,只要将之打印出来即可。以下 • 的递归算法就很容易理解了

  21. void PERMU(M) • { • IF (M= =1) PRINT LIST; • ELSE (FOR K=M;K>= l;K--) • {SWAP(K,M); • PERMU(M - 1); • SWAP(K,M) • } • }

  22. 注意,每一个递归调用都是条件执行的,只有这样才能防止无限循环注意,每一个递归调用都是条件执行的,只有这样才能防止无限循环 • 此为递归出口. 否则, 没有出口, 运行就陷入无限循环

  23. §4.2 栈 • 递归就其本质而言,是算法的循环执行,因此可改写为非递归的函数。大致讲, • 过程中要有一个循环结构,这个循环结构包含整个递归算法。原先的递归调用语句则改写为

  24. 1.参量的值的重新赋值,然后 • 2.转入过程的某一部分,继续执行

  25. 考虑折半搜寻问题。上节的递归算法BSEARCH中两次递归调用BSEARCH.考虑折半搜寻问题。上节的递归算法BSEARCH中两次递归调用BSEARCH. • 在非递归算法中,需要局部变量LEFT和RIGHT.第一次递归调用 • BSEARCH(X,M+1,R) • 改写为 • LEFT=M+1 • 再转入循环体的开始(因为循环体无条件执行)。第二次递归调用 • BSEARCH(X,L,M - 1) • 改写为 • RIGHT=M - l • 再转入循环体的开始。从而,BSEARCH的非递归过程BSEARCHl如下所示。

  26. 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); • }

  27. 过程BSEARCH_l与原来的递归过程BSEARCH结构上很相似。do-while循环语过程BSEARCH_l与原来的递归过程BSEARCH结构上很相似。do-while循环语 • 句包含了整个递归函数的函数体, • 其中加入了两个return,这两个return出现在不含 • 有递归调用的条件选择部份的末尾; • 递归调用语句改写成参量的重新赋值。

  28. 这种直观的方法可用来改写一类递归过程, • 此类过程的特征为:每一条件选择程序 • 段至多含有一个递归调用语句, • 且该递归调用语句是过程的最后一个语句。这种递归称为尾部递归, • 尾部递归的非递归化可按下述步骤进行

  29. 1.把递归算法整个的过程体做成循环语句的循环体1.把递归算法整个的过程体做成循环语句的循环体 • 2.在不包含递归调用的条件分枝末尾加EXIT语句 • 3.将递归调用语句替换成其参量的重新赋值,再转入过程的某一点

  30. 但是大多数递归问题不属于尾部递归(如快速排序、河内塔问题及排列问题)。但是大多数递归问题不属于尾部递归(如快速排序、河内塔问题及排列问题)。 • 有些条件分枝含有多个递归调用;有些递归调用语句之后、该条件分枝结束以前还有操作要执行。 • 这就需要将一些参量的值、局部变量的值恢复为递归调用语句之前的值, • 以使前述的一些工作、语句得以正确执行。

  31. 例如河内塔问题的递归函数TOWER中,两次递归调用TOWER.在第一次调用例如河内塔问题的递归函数TOWER中,两次递归调用TOWER.在第一次调用 • TOWER(其第一个实参值为N - 1)的执行过程中,很可能再次调用TOWER,参量N的 • 值会变化。但是毫无疑问,在执行打印语句 • WRITELN(N,A,C) • 时,要求N的值为第一次调用TOWER之前的值。在执行第二次递归调用 • TOWER(N - 1,B,A,C) • 时也有同样的要求。

  32. 又例如递归函数PERMU中,递归调用PERMU,从而, K的值会因为PERMU • 再次调用而发生变化,但在递归调用PERMU之后的语句 • SWAP(K,M) • 执行时,K所需要的值是递归调用PERMU之前K所具有的值。

  33. 由此,非尾部递归类型问题的关键在于:如果一个变量的值在递归调用结束后还会由此,非尾部递归类型问题的关键在于:如果一个变量的值在递归调用结束后还会 • 用到,则在该递归调用执行之前,必须保存这个值;在该递归调用结束时,必须将这个 • 值恢复,即将保存的值恢复。C++等语言有自动保存、恢复措施。

  34. 过程BSEARCH的变量值无需保护, • 这是因为递归调用后没有任何工作、语句要执行

  35. 递归调用的执行,一般会导致在该次调用结束前,此函数再次调用一次或数次。递归调用的执行,一般会导致在该次调用结束前,此函数再次调用一次或数次。 • 在此逐次调用的过程中,后保存的值一定比先保存的值先恢复, • 换言之,先保存的值一定比后保存的值后恢复。 • 显然递归过程的非递归化所需要的数据结构为“栈”。

  36. 递归过程中每一个需要保存的变量都要设置一个栈,因此可能有多个栈,递归过程中每一个需要保存的变量都要设置一个栈,因此可能有多个栈, • 但是所有这些栈用同一个指针。 • 除了变量、参量需要栈以外, • 还要设置一个返回栈,用于记载递归调用运行结束后,应回何处继续执行的信息(即记载返回点的信息) • 这里所说的返回, 和C++的return不同.

  37. 递归过程的开始点、结束点和返回点 • 统称关键点。递归函数TOWER有四个关键点。

  38. 河内塔问题的函数 • 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); ③ • } • ④}

  39. 很明显,最后两个关键点可以合并, • 因为第二次递归调用执行完毕,整个过程就执行结束了。

  40. 非递归化,就是要把函数改写为等价的但是没有递归调用的函数非递归化,就是要把函数改写为等价的但是没有递归调用的函数 • 具体到函数TOWER,就是要把函数体中的两个TOWER用等价的语句来替代 • 在第一个关键点处要做的工作为: 将栈清零,SP=0 给诸局部变量赋值

  41. 在最后一个关键点处要做的工作为: 调用过程POP,恢复保存在栈中的值 • 将递归算法改建为一个循环语句。 • 循环语句结束的条件为“栈空”。

  42. 具体到函数TOWER,就是要把函数体中的两个TOWER用等价的语句来替代具体到函数TOWER,就是要把函数体中的两个TOWER用等价的语句来替代 • 要保存此时变量的值,保存返回的地点;并且给局部变量赋值

  43. 函数TOWER_l调用三个过程: • SWAP(X,Y)的功能是交换X和Y的值, • POP的功能是恢复保护在栈中的值, • PUSH的功能是将值保存到栈去。

  44. 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的值指示着返回点} }

  45. Void POP( ) • {NUM=NUM_STACK[SP]; • X:=X_STACK[SP]; • Y:=Y_STACK [SP]; • Z:=Z_STACK[SP]; • CP=CP_STACK [SP]; • SP--; • }

  46. 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); }

  47. CASE 1: • IF (NUM>0) • { PUSH (2); • NUM--; • SWAP (Y,Z); • CP=1 //转向循环的开始处 • } • else CP=4; //转向调用过程POP • break;

  48. 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;

  49. 非递归化所应遵循的步骤如下: • 1. 设置所需的局部变量及分枝控制变量CP; • 2. 确定递归过程中的各关键点; • 3. 过程开始处给局部变量赋值并清栈; • 4. 将整个递归过程的过程体包含在循环语句中,它包含多个条件分枝,由CP的值确定哪个分枝应被执行; • 5. 将各关键点之间的语句放入不同的选择分枝中;

  50. 6.在不含递归调用的分枝末尾调用过程POP; • 7.将尾部递归替换成其参量的赋值语句,并转移至第一个关键点; • 8.对非尾部递归语句,要将参变量的值及返回点进栈,再转移至第一个关键点; • 9.在每个分枝的末尾给分枝控制变量CP赋值,以指明执行的转向; • 10.递归算法的结束处,调用过程POP; • 11.循环语句的结束条件为栈空。

More Related