第3章栈和队列 栈、队列、优先级队列和双端队列是4种特殊的线性表,它们的逻辑结构与线性表相同, 只是其运算规则较线性表有更多的限制,故又称它们为运算受限的线性表。栈和队列被广 泛应用于各种系统的程序设计中。 3.栈 1 栈是一种最常用和最重要的数据结构,它的用途非常广泛。例如,汇编处理程序中的句 法识别和表达式计算就是基于栈实现的。栈还经常使用在函数调用时的参数传递和函数值 的返回。 3.1 栈的定义 1. 通常,栈(stack)可定义为只允许在表的末端进行插入和删除的线性表。允许插入和删 除的一端称为栈顶(top),而不允许插入和删除的另一端称为栈底(botom )。当栈中没有任 何元素时则成为空栈。 设给定栈S=(a2,…,),则称最后加入栈中的元 a1,an 素an 为栈顶。栈中按a1,a2,…,an 的顺序进栈。而退栈的 顺序反过来,先退出,然后an-1才能退出,最后退出a1。换 句话说,后进者(a) 先出。因此,(n) 栈又称为后进先出( lastinfirst out,LIFO)的线性表,如图3.1所示。图3.与线性表类似,可以借助C++类来定义栈的抽象数据类1 栈 型,如程序3. 1所示。 程序3. 1 栈的类定 义 constintmaxSize=50; enol{astu umbofle,re} ; template clasStack{ //栈的类定 义 public: Stack(){}; //构造函 数 virtualvoidPsh(T&x)=0; //新元素x进 栈 virtualbolPop(T(u) &x)=0; //栈顶元素出栈,由x返 回 virtualboolg(o) etTop(T&x)st=0; //读取栈顶元素,由x返 回 virtualboolIsEmpty()st(c) =(o) 0;(n) //判断栈空 否 virtualboolIsFl()cost(c) =(n) 0;(o) //判断栈满 否 }; virtualintgetSize(u) ()const(n) =0; //计算栈中元素个数 ·81· 3.2 顺序栈 1. 栈的抽象数据类型有两种典型的存储表示:基于数组的存储表示和基于链表的存储表 示。基于数组的存储表示实现的栈称为顺序栈,基于链表的存储表示实现的栈称为链式栈。 顺序栈可以采用顺序表作为其存储表示,为此,可以在顺序栈的声明中用顺序表定义它 的存储空间。本书为简化问题起见,使用一维数组作为栈的存储空间。存放栈元素的数组 的头指针为*elements,该数组的最大允许存放元素个数为maxSize,当前栈顶位置由数组 下标指针top指示。如果栈不空时elements[0]是栈中第一个元素。 程序3. 2 顺序栈的类定 义 #inldseth> cue constintmaxStack=20; constintstackIncreament=20; //栈溢出时扩展空间的增 量 template clasSeqStack{(a) //顺序栈的类定 义 public: Setk(nz=50); //建立一个空栈 qScits ~SeqSt(a) ck(){dlete[]elements;} //析构函 数 idPsh(T(a) &x(a) (r) ) ; //(v) 如(o) 果Is(u) Ful(),则溢出处理;否则把x插入栈的栈 顶 blPop(T&x);//如(o) 果Is(o) Empty(),则不执行退栈,返回false;否则退掉位于栈顶的元素,返回true, //退出的元素值通过引用型参数x返 回 blgetTop(T&x);//如(o) 果Is(o) Empty(),则返回false;否则返回true,并通过引用型参数x得到栈顶元素的 值 blIEmpt{tn(op== -re:flse; } ty()t1)?tua//如(o) 果栈(s) 中元素个(c) 数(n) 等于(r) 0,(r) (u) (e) (s) (o) 则返回true,否则返回false blIl()t{n(o==xie1)?tre:fle; } FttpmaSz-uas//如(o) 果栈(s) 中(u) 元素(c) 个(n) 数等(r) 于(u) ma(r) (e) (s) (o) (o) xSize,则返回true,否则返回false intgetSize()(rtrntop+1;) //函数返回栈中元素个 数 voidMakEmpttop=-//清空栈的内容 y(){(u) (e) 1; } fidota(e) m&optr<<(o) (ostream&os,SeqStack&s);//(r) 输(e) 出(n) 栈(s) 中(r) 元(e) 素的重载(e) 操(o) 作 private: T*elements; //存放栈中元素的栈数 组 inttop; //栈顶指 针 intmaxSize; //栈最大可容纳元素个 数 }; voidoverflowProces(); //栈的溢出处理 栈的构造函数用于在建立栈的对象时为栈的数据成员赋初值。函数中动态建立的栈数 组的最大尺寸为maxSize,由函数参数sz给出,并令top=-1,置栈为空。在这个函数实 现中,使用了一种断言(asert)机制,这是C+ + 提供的一种功能。若断言语句asert参数 ·82· 表中给定的条件满足,则继续执行后续的语句;否则出错处理,终止程序的执行。这种断言 语句格式简洁,逻辑清晰,不但降低了程序的复杂性,而且提高了程序的可读性。 程序3. 3 顺序栈的构造函 数 template SqSk∷Stk(nz):o-xie(z) { tqScitstp(1),maSzs//建(e) 立一(a) 个最大尺寸(e) 为sz(a) (c) 的空栈,若分配不成功则错误处 理 elnts=newT[Size]; //创建栈的数组空 间 }; asert(e(eme) lements!=NU(ma) L(x) L); //断言:动态存储分配成功与 否 top指示的是最后加入的元素的存储位置。在实现进栈操作时,应先判断栈是否已满。 栈的最后允许存放位置为maxSize-1,如果栈顶指针top==maxSize-1,则说明栈中所 有位置均已使用,栈已满。这时若再有新元素进栈,将发生栈溢出,程序转入溢出处理。如 果top idSqStk∷oflowProces(){ //(v) 私(o) 有函(e) 数:(a) 扩充栈的存(v) (c) 储(e) 空间(r) T*newAray=wT[xSize+stackIncreament]; if(ewAray==N(n) U(e) LL){(ma) r<<"存储分配失败!"< idSqStk∷Ph(T&x){//(v) 共(o) 有函(e) 数:(a) 若栈不满则(u) 将(s) 元素x插入该栈的栈顶,否则溢出处理(c) if(sl()==uefowPoes();//栈满则溢出处 理 IFte)orlrc }; elements(u) [++top]=(r) x;(v) //栈顶指针先加1,再进 栈 template blSqStk∷Pp(T&x){//共(o) 有函(e) 数:(a) 若栈不空则(o) 函数返回该栈栈顶元素的值,然后栈顶指针减1(c) (o) if(stte)eunfle; //判断栈空否,若栈空则函数返回 IEmpy()==urtras x=lttp//栈顶指针减 1 ens[o--];(r) }; returntrue;(eme) //退栈成 功 template blSqStk<(a) T(s) >∷gtTop(T&x){//共(o) 有函(e) 数:(a) 若栈不空(c) (o) 则(e) 函数返回该栈栈顶元素的地 址 if(stte)eunfle; //判断栈空否,若栈空则函数返回 IEmpy()==urtras returnelementtop];(r) }; returntrue; s[//返回栈顶元素的值 template tam&optr<<(stream&os,SeqStack&s) { //(o) 输(s) 出(e) 栈中元素(e) 的(a) 重载操(o) 作(o) os<<"top="<2) 复杂,在插入时元素的移动量很大,因而时间代价较高。特别是当整个存储空间即将充满 时,这个问题更加严重。解决的办法就是采用链接存储表方式作为栈的存储表示。 1.链式栈 3.3 链式栈是线性表的链接存储表示。采用链式栈来表示一个栈,便于结点的插入与删除。 在程序中同时使用多个栈的情况下,用链接存储表示不仅能够提高效率,还可以达到共享存 储空间的目的。 从图3.4可知,链式栈的栈顶在链表的表头。因此,新结点的插入和栈顶结点的删除都 在链表的表头,即栈顶进行。下面给出链式栈的类声明。由于第2章给出的单链表结点是 ·85· 图3. 4 链式栈 用stut定义的,在链式栈的情形中可以直接使用,所以在程序3. rc6中没有定义链式栈的 结点。 程序3. 6 链式栈的类定 义 #inlde #inlde"Lith//使用了单链表结 点 cus. " template clasLinkedStack{ //链式栈类定 义 public: LinkedStack():top(NULL){} //构造函数,置空栈 ~LinkdStack(){makeEmpty();}; //析构函数 voidPsh(T(e) x); //进栈 bolPop(T(u) &x); //退栈boolg(o) etTop(T&x)st; //读取栈顶元素 bolIEmpy()srtrtpNULL)?tuae;} ostt{(n) (o) (c) eun(o==re:flintgetSie()ost;(n) (o) (c) /(s) /求栈的元素个数 voidmakeEmpt(c) (z) y();(n) //清空栈的内容 fidotam&optr<<(ostream&os,LinkedStack&s); //(r) 输(e) 出(n) 栈(s) 中(r) 元(e) 素的重载(e) 操(o) 作 private: }; LinkNode *top; //栈顶指针,即链头指 针 template idLikdStk∷makEmpty() { //(v) 逐(o) 次删(n) (a) (r) 去(e) 链式(a) 栈中的元素(c) 直至(e) 栈顶指针为 空 LinkNode *p; whie(oNULL) ltp!=//逐个结点释 放 {p=top;top=top->link;deletep; } } ; template idLikdStk∷Ph(Tx) { //(v) 将(o) 元素(n) 值(e) x插(a) 入链式栈的栈(u) 顶,(s) (c) 即链 头 LinkNode*s=newLinkNode(x); //创建新的含x结 点 if(s==NULL){cer<<"存储分配失败!"<link=top;toop=s; }; template blLikdStk∷Pp(T&x){ //删(o) 除栈(n) 顶(e) 结点(a) ,返回被(c) (o) 删栈(o) 顶元素的值 ·86· f(y()==e)e; //若栈空则不退栈,返回 LinkNode *p=top;(r) //否则暂存栈顶元素 top=top->link; //栈顶指针退到新的栈顶位置 x=p->data;deletep; //释放结点,返回退出元素的值 returntrue; iIsEmpttrueturnfals }; template blLikdStk(s) ∷getTop(T&x)const{ //返(o) 回栈(n) (o) 顶(e) 元素(a) 的值(c) iIsEmpttrureturnfalsl f(y()==e)e; //若栈空则返回fx=top->data; //栈不空则返回栈顶(a) 元(e) 素的值 returntrue; }; template intLinkedStack(a) <(s) T>∷getSize() { LinkNode *p=top;intk=0; whitNULL){o=oik;k++; } le(op!=tptp->ln returnk; } ; template tam&o(s) ptr<<(stream&os,LinkedStack&s) { //(o) 输(s) 出(e) 栈中元素(e) 的(a) 重载操(o) 作(o) os<<"栈中元素个数=< =t" p;in.t0i; e()<data<<"";p=p->link; } os< *s=newLinkNode[n]; 在多个链式栈的情形中,k域需要一些附加的空间,但其代价并非很大。 lin **3.4 栈的应用之一———括号匹配 1. 举例说明,在一个字符串(b+c)d)中位置1和位置4有左括号“(,(”) 位置8和 位置11有右括号“)”。位置1的左括号匹配位置11的右括号,位置4的左括号匹配位置8 的右括号。而对于字符串"(,(") 位置7的左 "a*(-" 括号没有可匹配的右括号。 a+b))(位置6的右括号没有可匹配的左括号, 我们的目的是建立一个算法,输入一个字符串,输出匹配的括号和没有匹配的括号。 ·87· 可以观察到,如果从左向右扫描一个字符串,那么每个右括号将与最近遇到的那个未匹 配的左括号相匹配。这个观察的结果使我们联想到可以在从左向右的扫描过程中把所遇到 的左括号存放到栈中。每当在后续的扫描过程中遇到一个右括号时,就将它与栈顶的左括 号(如果存在)相匹配,同时在栈顶删除该左括号。程序3.7给出相应的算法,其时间复杂度 为O(n)或Θ(n),其中n 是输入串的长度。 程序3.7 判断括号匹配的算法 #include #include #include #include"SeqStack.cpp" constintmaxLength=100; //最大字符串长度 voidPrintMatchedPairs(char*expression){ SeqStacks(maxLength); //栈s存储 intj,length=strlen(expression); for(inti=1;i<=length;i++){ //在表达式中搜索“(”和“)” if(expression[i-1]== '(')s.Push(i); //左括号,位置进栈 elseif(expression[i-1]== ')'){ //右括号 if(!s.IsEmpty()&&s.Pop(j)) //栈不空,退栈成功 cout< <操作符> <操作数>。例如,A +B。 (2)前缀(prefix)表示:<操作符> <操作数> <操作数>。例如,+AB。 (3)后缀(postfix)表示:<操作数> <操作数> <操作符>。例如,AB+。 我们平时所使用的表达式都是中缀表示。下面就是表达式的中缀表示: ·88· A +B*(C-D)-E/ F 为了正确执行这种中缀表达式的计算,必须明确各个操作符的执行顺序。为此,为每个 操作符都规定了一个优先级,如表3. 1所示。一般表达式的操作符有4种类型:①算术操作 符,如双目操作符(+、-、*、/和%)以及单目操作符(-)。这些操作符主要用于算术操作 数。②关系操作符,包括<、<= 、== 、!=、>= 、>。这些操作符主要用于比较,不但适用 于算术操作数,而且适用于字符型操作数。③逻辑操作符,如与(&& )、或(|| )、非(!)。 ④括号“(”和“)”。它们的作用是改变运算顺序。操作数可以是任何合法的变量名和常数。 表3.+中操作符的运算优先级 1C+ 优先级1 2 3 4 5 6 7 操作符-(单目),! *,/,% +,-<,<=,>,>= ==,!= && || C++规定一个表达式中相邻的两个操作符的计算次序:优先级高的先计算;如果优先 级相同,则自左向右计算;当使用括号时,从最内层的括号开始计算。 由于中缀表示中有操作符的优先级问题,还有可加括号改变运算顺序的问题,所以对于 编译程序来说,一般不使用中缀表示处理表达式。解决办法是用后缀表示(较常用)和前缀 表示。因为用后缀表示计算表达式的值只用一个栈,而前缀表示和中缀表示同时需要两个 栈,所以编译程序一般使用后缀表示求解表达式的值。 例如,日常使用中缀表达式 A + B *(-E/F,5所示, R1,R2,R3,R4,R5为中间计算结果。 C- D )计算的执行顺序如图3. 2. 应用后缀表示计算表达式的值 中缀表示是最普通的一种书写表达式的形式,而且在各种程序设计语言和计算器中都 使用它。用中缀表示计算表达式的值需要利用两个栈来实现:一个暂存操作数;另一个暂 存操作符。利用"stch中定义的模板Stck类,建立两个不同数据类型的Sak对象。 ak." atc 下面讨论的是利用后缀表示计算表达式的值。后缀表示也称RPN 或逆波兰记号,它 是中缀表示的替代形式,参加运算的操作数总在操作符前面。例如,中缀表示 A + B * (C-D)-E/ F 所对应的后缀表示为ABCD-*+EF/-。 利用后缀表示计算表达式的值时,从左向右顺序地扫描表达式,并使用一个栈暂存扫描 到的操作数或计算结果。例如,与图3. 5所示的中缀表达式计算等价的后缀表达式计算顺 序如图3.因而括号在 6所示。在后缀表达式的计算顺序中已经隐含了加括号的优先次序, 后缀表达式中不出现。 图3.5 中缀表达式的计算顺序图3. 6 后缀表达式的计算顺序 本节的讨论只涉及双目操作符,不考虑单目操作符。 通过后缀表示计算表达式值的过程(见图3.:顺序扫描表达式的每项,然后根据它的 7) ·89· 类型做如下相应操作:如果该项是操作数,则将其压入栈中;如果该项是操作符,则 连续从栈中退出两个操作数 Y 和 X ,形成运算指令 X Y,并将计算结果重新压入栈 中。当表达式的所有项都扫描并处理完后,栈顶存放的就是最后的计算结果。 步扫描项项类型动作栈中内容 1 置空栈空 2 A 操作数进栈 A 3 B 操作数进栈AB 4 C 操作数进栈ABC 5 D 操作数进栈ABCD 6-操作符D、 C 退栈,计算C-D,结果R1 进栈ABR1 7* 操作符R1、 B 退栈,计算B*R1,结果R2 进栈AR2 8+ 操作符R2、 A 退栈,计算A+R2,结果R3 进栈R3 9 E 操作数进栈R3E 10 F 操作数进栈R3EF 11 / 操作符F、 E 退栈,计算E/F,结果R4 进栈R3R4 12 -操作符R4、R3 退栈,计算R3-R4,结果R5 进栈R5 图3. 7 通过后缀表示计算表达式值的过程 下面通过模拟一个简单的计算器的+,-,*,/等运算,进一步说明后缀表达式的求值 问题。计算器接收浮点数,计算表达式的值。计算数据和操作都包含在类Calculator中,通 过一个简单的主程序来调用类的成员函数进行计算。 程序3.r类的定义 8Calculato #inldth> cue cusram. #inlde"Stk.pp cuqS(o) cc" lClltr{(a) (e) //(c) 模(a) 拟一(a) 个(c) 简(a) 单(o) 的计算器。此计算器对从键盘读入的后缀表达式求值(u) (s) public: luaoitsz):z){} //构造函数 Cacltr(ns(sdoubleRun(chare[]); //执行表达式计算 voidClear(); private: SqStcks; //栈对象定 义void(e) Ad(a) dOperand(doublevalue); //进操作数 栈 blGet2Opends(double&left,double&right); //从栈中退出两个操作 数 }; void(o) DoOp(o) erator(c(a) (r) harop); //形成运算指令,进行计算 因为计算器只要开机就一直运行着,所以需要在开始计算表达式之前先调用成员函数 Clear()将栈清空。然后执行成员函数Run()输入一个后缀表达式,输入流以#结束。程序 3.acltr类各私有成员函数的实现。 9给出Cluao ·90· 程序3.9 Calculator类私有成员函数的实现 voidCalculator∷DoOperator(charop){ //私有成员函数:取两个操作数,根据操作符op形成运算指令并计算 doubleleft,right,vlaue;boolresult; result= Get2Operands(left,right); //取两个操作数 if(result==true) //如果操作数取成功,计算并进栈 switch(op){ case'+':value=left+right;s.Push(value);break; //加 case'-':value=left-right;s.Push(value);break; //减 case'*':value=left*right;s.Push(value);break; //乘 case/' ':if(right==0.0){ //除 cerr<<"Divideby0!"<isp(op),令ch进栈,读入下一个字符ch。 . 若icp(ch)icp(')'),退栈 ' (' ==' )' ,退栈 #+*( #+* ABCD - ABCD - 10 - 操作符isp('*')>icp('-'),退栈 isp('+')>icp('-'),退栈 isp('#')icp('#'),退栈 isp('-')>icp('#'),退栈 结束 #- # ABCD -*+EF/ ABCD -*+EF/- 图3.8 利用栈的转换过程 3.2 栈与递归 3.2.1 递归的概念 递归(recurve)在计算机科学和数学中是一个很重要的工具,它在程序设计语言中用来 定义句法,在数据结构中用来解决表或树形结构的搜索和排序等问题。数学家在研究组合 问题时用到递归。递归是一个重要的课题,在计算方法、运筹学模型、行为策略和图论的研 ·93· 究中,从理论到实践,都得到了广泛的应用。本节将对递归做简要的说明,并举例说明它的 各种应用。在以后各章中将利用它来研究诸如树、搜索和排序等问题。 例如,在计算浮点数x 的n(自然数)次幂时,一般可以把它看作n 个x 连乘: xn =x ×x ×x × … ×x ×x .......................... n个 而在求前n 个自然数的和时,则可以把它看作是n 个自然数的连加: S(n)=Σn i=1 i=1+2+3+ … + (n -1)+n 如果已经求得xn 或S(n),那么在计算xn+1或S(n+1)时,可以直接利用前面计算过的 结果以求得答案:xn+1=xn ×x 或S(n+1)=S(n)+ (n+1)。这样做既简洁又有效。 像这种利用前面运算来求得答案的过程称为递归过程。 在数学及程序设计方法学中为递归下的定义:若一个对象部分地包含它自己,或用它 自己给自己定义,则称这个对象是递归的;而且若一个过程直接地或间接地调用自己,则 称这个过程是递归的过程。 在以下3种情况下,常常要用到递归的方法。 1.定义是递归的 数学上常用的阶乘函数、幂函数、斐波那契数列等,它们的定义和计算都是递归的。例 如阶乘函数,它的定义为 n! = 1, n =0 n(n -1)!, n >0 { 对应这个递归的函数,可以使用递归过程来求解。 程序3.11 计算阶乘的递归函数 longFact(longn){ if(n==0)return1; //终止递归的条件 elsereturnn*Fact(n-1); //递归步骤 } 在程序3.11中利用了if…else…语句把递归结束条件与其他表示继续递归的情况区 别开来。if语句块判断递归结束的条件,而else语句块处理递归的情况。在计算n! 时,if 语句块判断唯一的递归结束条件n = 0,并返回值1;else语句块通过计算表达式n(n- 1)!,并返回计算结果以完成递归。 图3.9描述了执行Fact(4)的函数调用顺序。假设最初是主程序main()调用了函数。 在函数体中,else语句以参数3,2,1,0执行递归调用。最后一次递归调用的函数因参数n =0执行if语句。一旦到达递归结束条件,调用函数的递归链中断,同时在返回的途中计 算1*1,2*1,3*2,4*6,最后将计算结果24返回给主程序。 还可以举出一些函数递归定义的例子。但仅从以上两个例子中已经可以得到以下三点 认识。 (1)对于一个较为复杂的问题,当能够分解成一个或几个相对简单的且解法相同或类 似的子问题时,只要解决了这些子问题,原问题就迎刃而解了。这就是分而治之的递归求解 方法,也称减治法或分治法。参看图3.9,计算4!时先计算3!。只要求出3!,就可以求 ·94· 图3. 9 求解阶乘n!的过程 出4!。 (2)当分解后的子问题可以直接解决时,就停止分解。这些可以直接求解的问题称为 递归结束条件。如图3.1。 9中递归结束条件是0!= (3)递归定义的函数可以用递归过程来编程求解。递归过程直接反映了定义的结构。 2.数据结构是递归的 某些数据结构就是递归的。例如,链表就是一种递归的数据结构。链表结点LinkNode 的定义由数据域data和指针域link组成;而link则由LinkNode定义。 从概念上讲,可将一个头指针为first的单链表定义为一个递归结构: (1)first为NULL,是一个单链表(空表); (2)first≠NULL,其指针域指向一个单链表,仍是一个单链表。 对于递归的数据结构,采用递归的方法来编写算法特别方便。例如,搜索非空单链表最 后一个结点并返回其地址,就可以使用递归形式的过程。 程序3. 12 搜索单链表最后一个结点的算 法 template LinkNode *FidRer(LinkNode *f) { f()(n) L; if==NULLretrn(a) NUL sf(ikN(u) r eleif->ln==ULL)trnf; elertridRaf->ln seunFner(ik);(u) (e) } ; 如果f->link==NULL,表明 f 已到达最后一个结点,此时可返回该结点的地址, 否则以f->link为头指针继续递归执行该过程。 又例如,在一个非空单链表中搜索其数据域的值等于给定值 x 的结点,并在首次找到 时返回其结点地址。在此算法中,递归结束条件有两个:①链表已经全部扫描完但没有找 到满足要求的结点,此时f==NULL;②在f!=NULL同时f->data==x时找到要 求的结点。 ·95· 程序3. 13 在以f为头指针的单链表中搜索其值等于给定值x的结 点 template LinkNode *Sch(LinkNode *f,T&x) { f()(r) n; //搜索失败 if==NULL(a) (e) retr sf(aax)(u) e//搜索成 功 eleif->dt==rtrnf; }; elertrerh(ik(u) ,x); //从下一个结点开始继续搜 索 seunSacf->ln 不仅是单链表,第5章介绍的树形结构,是以多重链表作为其存储表示的,它也是递归 的结构。所以关于树的一些算法,也可以用递归过程来实现。 3.问题的解法是递归的 有些问题只能用递归方法来解决。一个典型的例子就是汉诺塔(TowerofHanoi)问 题。问题的提法:“传说婆罗门庙里有一个塔台,台上有3根标号为A,B, C 的用钻石做成 的柱子,在 A 柱上放着64个金盘,每个都比下面的略小一点。把 A 柱上的金盘全部移到 C 柱上的那一天就是世界末日。移动的条件是一次只能移动一个金盘,移动过程中大金盘不 能放在小金盘上面。庙里的僧人一直在移个不停。因为全部的移动是263-1次,如果每秒 移动一次,需要500亿年。” 一位计算机科学家提出了一种快速求解汉诺塔问题的递归解法。用图解来示意4个盘 子的情形。设 A 柱上最初的盘子总数为n,问题的解法如下。 如果 n =1,则将这一个盘子直接从 A 柱移到 C 柱上。否则,执行以下3步: (1)用 C 柱做过渡,将 A 柱上的n-1个盘子移到 B 柱上; (2)将 A 柱上最后一个盘子直接移到 C 柱上; 柱上 ( 。 3)用 A 柱做过渡,将 B 柱上的n-1个盘子移到 C 移动过程如图3.10所示,图中 n =4。利用这个解法, 将移动 n 个盘子的汉诺塔问题归结为移动n-1个盘子的 汉诺塔问题。与此类似,移动n-1个盘子的汉诺塔问题又 可归结为移动n-2个盘子的汉诺塔问题……最后总可以 归结到只移动一个盘子的汉诺塔问题,这样问题就解决了。 现在给出根据上述解法而得到的求解 n 阶汉诺塔问题 图3.的算法。 10 汉诺塔问题的解答 程序3. 14 求解n阶汉诺塔问题的算 法 #inlde vodHaoitn,hrA,hrB,hrC) { ini(ncacaca if(n==1) cout<<"Movetopdiskfrompeg"<0 { 注意,递归与递推是两个不同的概念。递推是利用问题本身所具有的递推关系对问题 求解的一种方法。采用递推法建立起来的算法一般具有重要的递推性质,即当求得问题规 模为i-1的解后,由问题的递推性质,能从已求得的规模为1,2,…,i-1的一系列的解, 构造出问题规模为i 的解。若设这种问题的规模为n,当n =0或n =1时,解或为已知, 或能很容易地求得。例如,求n!,求等比级数的第n 项等。 递推问题可以用递归方法求解,也可以用迭代(重复)的方法求解。 3.2.2 递归过程与递归工作栈 在图3.9所示的例子中,主程序调用Fact(4)属于外部调用,其他调用都属于内部调用, 即递归过程在其过程内部又调用了自己。调用方式不同,调用结束时返回的方式也不同。 外部调用结束后,将返回调用递归过程的主程序。内部调用结束后,将返回到递归过程内部 本次调用语句的后继语句处。此外,函数每递归调用一层,必须重新分配一批工作单元,包 括本层使用的局部变量、形式参数(实际是上一层传来的实际参数的副本)等,这样可以防止 使用数据的冲突,还可以在退出本层,返回到上一层后恢复上一层的数据。 1.递归工作栈 为了保证递归过程每次调用和返回的正确执行,必须解决调用时的参数传递和返回地 ·97· 址问题。因此,在每次递归过程调用时,必须做参数保存、参数传递等工作。在高级语言的 处理程序中,是利用一个递归工作栈来处理的, 12 所示。 如图3. 图3. 12 函数递归调用时的活动记录 每层递归调用所需保存的信息构成一个工作记录。通常它包括如下内容。 (1)返回地址:即上一层中本次调用自己的语句的后继语句处。 (2)在本次过程调用时,为与形式参数结合的实际参数创建副本。包括传值参数和传 值返回值的副本空间,引用型参数和引用型返回值的地址空间。 (3)本层的局部变量值。 在每进入一层递归时,系统就要建立一个新的工作记录,把上述项目录入,加到递归工 作栈的栈顶。它构成函数可用的活动框架。每退出一层递归,就从递归工作栈退出一个工 作记录。因此,栈顶的工作记录必定是当前正在执行的这一层的工作记录。所以又称为活 动记录。 以图3.9所示的计算Fact(4)为例,介绍递归过程中递归工作栈和活动记录的使用。 参见程序3.at(的调用由主程序执行。当函数运行结束后控制返回到 15 。最初对Fc4) RetLoc1处,在此处将函数的返回值24(即4!) 赋予整型变量n,RetLoc1在赋值运算符“=” 处。函数Ft(4)递归调用Ft(3)时,调用返回处在R2。在此处计算n*(1)!, acacetLocnRetLoc2在乘法运算符“*”处。 程序3.15 计算阶乘的递归算法Fact(4) voidmain() { longn; // 调用Fact(4)时记录进 栈 n=Fact(4) ; RetLoc1 ↑ // 返回地址RetLoc1在赋值语 句 cout<S;Nodew;longsum=0;do{(n) whie(n>1){n=n;w.a=Psh(w);n--;} lw.tg1;S. u sum=sum+n; whie(IEmpy()==ase) { lS.stfl S.ow) ; Pp( if(tg==1) w.a{w.ta=Psh(w);n=w.-2;bek;} g2;S.unra } }whie(IEmpy()==ase) ; lS.stfl returnsum; } ; 该算法执行时栈的变化如图3.图中显示了各 15所示。每次大循环中包含两个小循环, 小循环执行后栈中的内容以及 n 的值和sum的计算。最后在sb(的值。 um中得到Fin) 图3.ib(时栈的变化 15 用栈求解斐波那契数列的第 n 项Fn) 3.用迭代法实现递归过程 在图3.计算Fib(需要先计算 14所示的计算斐波那契数列的递归调用树中, 4)时, Fib(3),再计算Fib(2);计算Fib(3)时,需要先计算Fib(2),再计算Fib(1)……因此,需重复 ·100·