版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
第3章堆栈和队列3.1堆栈3.2队列3.3*表达式的计算3.4*递归和递归过程3.5*演示和测试习题33.1堆栈3.1.1堆栈ADT堆栈(或栈)是限定插入和删除运算只能在同一端进行的线性数据结构。允许插入和删除元素的一端称为栈顶(top),另一端称为栈底(bottom)。若栈中无元素,则为空栈。若给定堆栈S=(a0,a1,…,an-1),则称a0是栈底元素,an-1是栈顶元素。若元素a0,…,an-1依次进栈时,则出栈的顺序与进栈相反,即元素an-1必定最先出栈,然后an-2才能出栈(见图3-1)。由于栈的这种后进先出的特点,因此栈是后进先出(LastInFirstOut-LIFO)的线性数据结构。
栈的基本运算包括构造一个空堆栈,判定一个栈是否为空栈,判定一个栈是否已满,在一个未满的栈中插入一个新元素,从一个非空的栈中删除栈顶元素等。当然我们还可以根据应用需要增加其它必要的栈运算,如求栈的长度,清除一个栈,以及遍历一个栈等。堆栈的抽象数据类型定义见ADT3-1。函数CreateStack是抽象数据类型的构造函数,有时可以根据需要定义多个构造函数。ADT3-1Stack{数据:零个或多个元素的线性序列(a0,a1,...,an-1),其最大允许长度为MaxStack。运算:
voidCreateStack(Stack*s,intmaxsize);构造一个空堆栈。
BOOLIsEmpty(Stacks)若堆栈为空,则返回TRUE,否则返回FALSE。
BOOLIsFull(Stacks)若堆栈已满,则返回TRUE,否则返回FALSE。
voidPush(Stack*s,Tx)若堆栈已满,则指示Overflow,否则值为x的新元素进栈,成为栈顶元素。
voidPop(Stack*s)若堆栈为空,则指示Underfow,否则栈顶元素从栈中删除。
voidStackTop(Stacks,T*x)若堆栈为空,则指示Underfow,否则在参数x中返回栈顶元素值。
}3.1.2堆栈的顺序表示我们已经知道两种最常用的数据存储表示方式:顺序表示和链接表示。当我们用一维数组存储栈时,被称为顺序栈(sequentialstack),图3-2是栈的顺序表示示意图。栈的链接表示在下一小节讨论。栈的顺序实现可以用下面的C语言结构定义:#defineMaxSize50#defineFALSE0#defineTRUE1typedefintBOOL;typedefintT;typedefstructstack{intTop,MaxStack;TElements[MaxSize];}Stack
在上面定义的结构类型Stack中,Top是栈顶指针,MaxStack为栈的最大允许长度,它应不大于整型常量MaxSize。一维数组Elements用以存放队列中的元素,实现堆栈的顺序存储。标识符BOOL被定义为整型。程序3-1是ADT3-1中规定的栈运算在顺序表示下的实现。当堆栈为空时,令栈顶指针Top=-1。栈的容量MaxStack由用户通过参数maxsize设定,但不能超过MaxSize。
进栈操作是:首先令栈顶指针进一(++s->Top),然后将新元素x存放在新的栈顶位置(s->Elements[++s->Top]=x)。出栈操作只是简单地令栈顶指针退一(s->Top--;)。所有这些运算均包含参数s。对于那些会改变堆栈内容的运算,参数s为指针类型Stack*,否则为Stack类型。主程序main用于测试已实现的栈运算,起着驱动程序的作用。请注意主程序应严格按照每个函数的参数类型调用它们。对于Stack*类型的参数,其对应的实参前必须加上取地址运算符&。主程序中使用的PrintStack(Stacks)函数,输出栈中元素值,我们将其留作练习,请自行实现之。PrintElement(x)函数的功能见2.3.2节。程序3-1顺序栈实现voidCreateStack(Stack*s,intmaxsize){s->Top=-1;s->MaxStack=maxsize;}BOOLIsEmpty(Stacks){returns.Top<0;}BOOLIsFull(Stacks){returns.Top>=s.MaxStack-1;}voidPush(Stack*s,Tx){if(IsFull(*s))printf("Overflow");elses->Elements[++s->Top]=x;}voidPop(Stack*s){if(IsEmpty(*s))printf("Underflow");elses->Top--;}voidStackTop(Stacks,T*x){if(IsEmpty(s))printf("Underflow");else*x=s.Elements[s.Top];}voidmain(void){Stacks;Tx;CreateStack(&s,10);/*构造一个容量为10的空整数栈*/Push(&s,10);Push(&s,15);/*在栈中依次压入元素10和15*/PrintStack(s);/*显示栈中元素*/x=*InputElement();Push(&s,x);/*调用InputElement函数接受新元素x,并令其进栈*/PrintStack(s);/*显示栈中元素*/Pop(&s);Pop(&s);/*在栈中依次弹出两个元素*/if(IsEmpty(s))printf("Isempty.");/*判断此时栈是否为空栈*/elseprintf("Isnotempty.");PrintStack(s);/*显示栈中元素*/}3.1.3堆栈的链接表示堆栈也可用单链表实现之,堆栈在链接存储方式下的实现称为链式栈(linkedstack),如图3-3所示。由于堆栈具有后进先出特性,所以,栈顶指针Top指示的结点是an-1,它是栈顶元素,堆栈的栈底元素是单链表的尾结点。其结点类型与单链表相同,Node类型和Stack类型分别定义如下:typedefstructnode{TElement;structnode*Link;}Node;typedefstructstack{Node*Top;}Stack;
在链接存储表示下,进栈和出栈如程序3-2所示。链式栈的其它栈运算留作练习,请自行实现之。链式堆栈的函数Push的步骤是:使用函数NewNode2(见程序2-4)构造一个新结点*p,并将其插在链表的最前面,成为头结点(p->Link=s->Top;s->Top=p;)。出栈函数Pop的步骤是:用局部指针p指示栈顶结点,令栈顶指针Top指向下一个结点(s->Top=p->Link;),最后释放结点*p(free(p))。请注意,释放空间语句free要求的参数是指针变量p,而不是指针所指向的结点*p。在栈的链接存储下,可以取消容量MaxStack的限制,但这会导致不同的构造函数。程序3-2链式栈的进栈和出栈运算voidPush(Stack*s,Tx){Node*p=NewNode2(x);p->Link=s->Top;s->Top=p;}voidPop(Stack*s){Node*p=s->Top;s->Top=p->Link;free(p);}
我们可以使用测试顺序栈完全相同的主程序作为测试链式栈的驱动程序。只要链式栈实现的各栈运算的接口(即函数原型)与ADT3-1Stack中定义的完全相同,则主程序中调用栈函数的语句可以不加任何改动。这就是所谓的使用与实现分离。实现的改变应不影响客户程序。这正是抽象数据类型的宗旨。3.2队列3.2.1队列ADT
队列是限定只能在表的一端插入元素,在表的另一端删除元素的线性数据结构。允许插入元素的一端称队尾(rear),允许删除元素的另一端称队头(front)。若队列中无元素,则称为空队列。若给定队列Q=(a0,a1,…,an-1),则称a0是队头元素,an-1是队尾元素。若元素a0,…,an-1依次入队列,则元素出队列的次序与入队列一致,a0出队列后,a1才能出队列,见图3-4。由于队列的这种先进先出的特点,因此队列是先进先出(FirstInFirstOut-FIFO)的线性数据结构。图3-4队列示意图
队列的基本运算包括构造一个空队列,判定一个队列是否为空队列,判定一个队列是否已满,在一个未满的队列中插入一个新元素,从一个非空的队列中删除队头元素等。当然我们还可以根据应用需要增加其它必要的队列运算,如求队列的长度,清除一个队列,以及遍历一个队列等。队列的抽象数据类型定义见ADT3-2。ADT3-2Queue{数据:零个或多个元素的线性序列(a0,a1,...,an-1),其最大允许长度为MaxQueue。运算:
voidCreateQueue(Queue*q,intmaxsize);已构造一个空队列。
BOOLIsEmpty(Queueq)若队列为空,则返回TRUE,否则返回FALSE。BOOLIsFull(Queueq)若队列已满,则返回TRUE,否则返回FALSE。
voidAppend(Queue*q,Tx)若队列已满,则指示Overflow,否则值为x的新元素进队列。voidServe(Queue*q)若队列为空,则指示Underflow,否则从队列中删除队头元素。voidQueueFront(Queueq,T*x)若队列为空,则指示Underflow,否则在参数x中返回队头元素值。}3.2.2队列的顺序表示与堆栈一样,队列可以采用顺序存储,也可以采用链接存储。当我们用一维数组存储队列时,被称为顺序队列(sequentialqueue),图3-5是队列的顺序表示示意图。队列的链接表示在下一小节讨论。队列的顺序实现可以用下面的C语言结构定义:typedefstructqueue{intFront,Rear,MaxQueue;TElements[MaxSize];}Queue图3-5队列的顺序表示
队列运算需要两个指针,Front指向队头元素,Rear指向队尾元素。MaxQueue为队列的最大允许长度,它应不大于整型常量MaxSize。一维数组Elments用以存放队列中的元素,实现队列的顺序存储。初始状态下,我们将Front和Rear两个指针均置成-1,代表空队列,见图3-6(a)。图中,f和r分别代表Front和Rear。
为了在队列中插入一个新元素,我们可以将指针Rear右移一个位置,在Rear指示的位置上存入新元素。图3-6(b)显示当在队列中依次插入20,30,40和50以后队列的状态。此时,Rear指示最后一个插入的元素50的位置,指针Front不动。从队列中删除元素的一种简单做法是将Front右移一个位置。这样,从队列中依次删除元素20,30和40后队列的状态见图3-6(c)所示。继续在队列中插入元素60,此时队列的状态如图3-6(d)所示。指针Rear已经移到最右边。按照这种简单的入队列和出队列运算的实现方法,此时我们已不允许再向队列中插入新元素,似乎队列已满。很明显,这种队列运算的简单实现方法有很大弊端,它造成很大的空间浪费。我们注意到Front和Rear指针始终是右移的,Front左边的存储空间被丢弃了。图3-6入队列和出队列运算的简单实现空队列;(b)元素20、30、40、50依次进队列;(c)元素20、30、40依次出队列;(d)元素60入队列如果我们将实现队列的数组从逻辑上看成是一个头尾相接的环,就能充分利用数组空间来存储队列元素了。图3-7显示了循环队列结构及其入队列和出队列运算后的状态。图3-7循环队列示意图(a)空队列;(b)元素20、30、40、50入队列;(c)元素20、30、40出队列;(d)元素60、70入队列初始状态下,我们将Front和Rear两指针均置成0。为了循环使用数组,可以利用取余运算符%计算新元素的插入位置(即新队尾元素的位置)和删除队头元素后的新队头元素的位置。队头指针进一操作:
Front=(Front+1)%MaxQueue;队尾指针进一操作:
Rear=(Rear+1)%MaxQueue;我们看到,指针Front和Rear始终以顺时针方向移动。在循环队列结构下,当Front==Rear时为空队列,当(Rear+1)%MaxQueue==Front时认为队列已满。满队列时实际上仍有一个元素空间未使用,其目的是为了区分空队列和满队列两种状态。如果不保留一个多余的元素空间,则当队列满的时候,也有Front==Rear。还有一种区分的做法是设立计数器,记录队列中元素的个数,当计数器的值达到MaxQueue时,表示满队列;当计数器为0时,表示空队列。当然这需要计数器空间,并且也耗费时间。程序3-3给出了循环队列定义及ADT3-2中规定的队列运算的实现。[程序3-3]循环队列实现voidCreateQueue(Queue*q,intmaxsize){ q->Front=q->Rear=0; q->MaxQueue=maxsize;}BOOLIsEmpty(Queueq){ returnq.Front==q.Rear;}BOOLIsFull(Queueq){return(q.Rear+1)%q.MaxQueue==q.Front;}voidAppend(Queue*q,Tx){if(IsFull(*q))printf("Overflow");elseq->Elements[q->Rear=(q->Rear+1)%q->MaxQueue]=x;}voidServe(Queue*q){if(IsEmpty(*q))printf("Underflow");elseq->Front=(q->Front+1)%q->MaxQueue;}voidQueueFront(Queueq,T*x){if(IsEmpty(q))printf("Underflow");else*x=q.Elements[(q.Front+1)%q.MaxQueue];}
与链式栈类似,队列的链接表示是用单链表来存储队列中元素,队头指针Front和队尾指针Rear分别指向队头结点和队尾结点,见图3-8。链接方式表示的队列称为链式队列(linkedqueue)。有了单链表和链式栈的基础,读者不难定义链式队列,并实现之。
typedefstructqueue{Node*Front,*Rear;}Queue;图3-8队列的链接表示(a)空队列;(b)非空队列链接方式表示的队列称为链式队列(linkedqueue)。有了单链表和链式栈的基础,我们不难定义链式队列:
typedefstructqueue{Node*Front,*Rear;}Queue;3.3表达式的计算
表达式计算(expressionevalution)是程序设计语言编译中的一个最基本问题,也是早期计算机语言研究的一项重要成果,它使得高级语言程序员可以使用与数学形式相一致的方式书写表达式。如a*b+c/d-c(x+y)。计算机科学计算语言FORTRAN因FormulaTranslator而得名。
计算一个表达式的难度在于它允许括号,并规定了运算符的优先级,使得表达式计算不能简单地从左向右进行。作为堆栈的应用实例,下面的方法将表达式计算过程分成两步:通过自左向右扫描表达式,将表达式转换成另一种形式;再对转换后的表达式,经自左向右扫描计算表达式的值。我们将看到这两步上都需使用堆栈。3.3.1表达式高级程序设计语言允许多种类型的表达式:算术表达式、关系表达式和逻辑表达式等。表达式由操作数、运算符和括号组成。表达式的习惯的书写形式是一个双目运算符(binaryoperator)位于两个操作数之间,如a+b,这类表达式称为中缀表达式(infixexpression)。除了双目运算符外,还有单目运算符(unaryoperator),如I++和-a。条件运算符是C语言中唯一的三目运算符(ternaryoperator)。为正确计算表达式的值,任何程序设计语言都明确规定了运算符的优先级。在C语言中,当使用括号时,从最内层括号开始计算;对相邻两个运算符,优先级高的先计算;如果两个运算符优先级相同,运算次序由结合方向决定。例如,*与/是自左向右结合,因此,a*b/c的运算次序是先乘后除。C语言的部分运算符的相对优先次序见表3-1(优先级数大的为高优先级)。3.3.2中缀表达式转换为后缀表达式尽管中缀表达式是普遍使用的书写形式,但编译程序通常需将中缀表达式转换成相应的后缀表达式后才求值。运算符在两个操作数之后的表达式称为后缀表达式(postfixexpression),又称逆波兰表达式(reversePolishform)。后缀表达式计算简便,它没有括号,运算符始终在两个操作数之后,表达式计算无须考虑运算符的优先级,只需从左向右一遍扫描后缀表达式,便可求得表达式的值。例如,对后缀表达式ab*c+,我们先计算ab*(即a*b),得到中间结果,设积为t,然后计算tc+(即t+c),得到该表达式的值。
中缀表达式包括三种类型的项:运算符、操作数和括号。设表达式以符号#结束。在处理中我们将左、右括号分别处理。观察表3-2,可以看到两种表达式的操作数的次序是相同的。下面给出将一个中缀表达式转换成后缀表达式的过程。转换算法需要堆栈作为辅助数据结构,堆栈在算法执行中用于存放运算符。初始时,先在栈的底部压入表达式结束符#。为了简化算法,我们只考虑左结合的双目运算。转换算法的输入为中缀表达式,该中缀表达式是由运算符、操作数、‘)’和‘#’四种不同类型的项(称为记号token)组成的序列,左括号的处理同运算符,将其归入运算符一类,表达式以#号结束。转换算法的输出得到由这些项组成的新序列,它们按后缀表达式次序排列,也以#号结束。转换算法自左向右逐项扫描作为输入的中缀表达式,根据不同类型的项,作不同的处理,产生输出项序列,生成后缀表达式。
设A是扫描输入中缀表达式时,得到的当前项,对A的处理方式为:
(1)若A=‘#’,则输出栈中剩余运算符,除栈底#号外,算法终止。
(2)若A是操作数,则将A加到输出序列的尾部。
(3)若A=‘)’,则从栈中不断弹出运算符,直至遇到左括号‘(’为止。令左括号出栈,A(即右括号)既不进栈,也不输出。
(4)若A是运算符,则将A的优先级与栈顶的运算符作比较,若A的优先级小于等于栈顶运算符的优先级,则从栈中弹出栈顶运算符,加到输出序列的尾部。重复执行这一操作,直到当A的优先级大于栈顶运算符的优先级时,令A进栈。
注意,在上面的处理中,我们对左括号的优先级作特殊规定:对未进栈的左括号赋以最高优先级,对已进栈的左括号赋以最低优先级。令#号的优先级为0。上述算法并未包括对中缀表达式的语法错误的检查部分,我们假定表达式的语法正确性检查已在表达式转换之前完成。
可以看到,上面的算法中,运算符优先级的比较决定了运算符的进、出栈操作,右括号无须进栈,左括号的处理同普通运算符,但在栈内和栈外被赋予不同的优先级。左括号有最高的栈外优先级和最低的栈内优先级,这样可保证左括号,连同括号内的其它运算符逐一进栈,直到遇到右括号为止。为此,我们为每个运算符(含左括号)规定了栈内优先级isp(in-stackprecedence)和栈外优先级icp(incomingprecedence),见表3-3所示。表中左括号的栈内优先级为0,栈外优先级为8,#号的栈内、外优先级均为0。表3-4是中缀表达式a/(b-c)+d*e转换为后缀表达式abc-/de*+的示意图。
程序3-4是将中缀表达式转换为后缀表达式的算法。为简单起见,我们只考虑操作数是一位十进制数或单字母变量,运算符为+、-、*、/的情况。函数InfixToPostfix完成将字符数组中保存的中缀表达式转换成后缀表达式输出。函数isp和icp实现对给定字符分别返回它的栈内优先级和栈外优先级。主函数main调用函数InfixToPostfix实现表达式转换。
函数InfixToPostfix首先创建一个空堆栈s,在栈底压入‘#’号。for循环依次读取(扫描)字符数组中的中缀表达式的项ch。若当前项(即字符ch)是字母或数字,则认为是操作数,直接输出该项到输出序列,函数isdigit和isalpha的原型包含在头文件ctype.h中,用于检测ch是否数字或字母。若ch是右括号,则不断弹出栈中保存的运算符,加到输出序列,直到弹出左括号为止(左、右括号不加入输出序列)。若ch是运算符(含左括号),则需与栈顶的运算符作比较,若ch的优先级小于等于栈顶运算符的优先级,则从栈中弹出栈顶运算符,加到输出序列,重复执行这一操作,直到当ch的优先级大于栈顶运算符的优先级时,令ch进栈。最后不断弹出栈中剩余运算符,将其加入输出序列,直到遇到‘#’,则转换算法结束。[程序3-4]中缀表达式转换为后缀表达式#include<ctype.h>#include"stack.h"/*包含一个堆栈数据结构*/#defineExpSize30intisp(charc){/*计算运算符c的栈内优先级*/intpriority;switch(c){ case'(':priority=0;break; case'+': case'-':priority=5;break; case'*': case'/':priority=6;break; case'#':priority=0;break;
}
returnpriority;}inticp(charc){/*计算运算符c的栈外优先级*/intpriority;switch(c){ case'(':priority=8;break; case'+': case'-':priority=5;break; case'*': case'/':priority=6;break; case'#':priority=0;break;}returnpriority;}voidInfixToPostfix(charexp[]){Stacks;inti;charch,y;CreateStack(&s,StackSize);/*构造一个空栈*/Push(&s,'#');/*栈底插入‘#’*/printf("\nThePostfixexpressionis:");for(i=0,ch=exp[i];ch!='#';i++,ch=exp[i]){if(isdigit(ch)||isalpha(ch))printf("%c",ch);/*输出操作数ch*/elseif(ch==')') for(StackTop(s,&y),Pop(&s);y!='(';StackTop(s,&y),Pop(&s)) intf(“%c”,y);/*输出栈中属于括号内的运算符*/else{for(StackTop(s,&y);icp(ch)<=isp(y);Pop(&s),StackTop(s,&y)) printf("%c",y);/*输出栈顶的运算符y,直到icp(ch)>isp(y)*/ Push(&s,ch);/*当前运算符ch进栈*/}}while(!IsEmpty(s)){/*输出栈中剩余运算符*/StackTop(s,&y);Pop(&s);if(y!='#')printf("%c",y);}}voidmain(){charexp[ExpSize]={'a','/','(','b','-','c',')','+','d','*','e','#'};InfixToPostfix(exp);}3.3.3计算后缀表达式的值利用栈很容易计算后缀表达式的值,其计算过程为:从左向右扫描后缀表达式,遇到操作数就进栈,遇到运算符就从栈中弹出两个操作数,执行该运算符所规定的运算,并将所得结果进栈。如此下去,直到表达式结束,弹出栈顶元素即为表达式的值。表3-5给出了后缀表达式abc-/de*+的计算过程(a=6,b=4,c=2,d=3,e=2)。类似地,表达式以#号结束。
程序3-5计算后缀表达式的值。为简单起见,我们只考虑操作数是一位十进制数,运算符为+、-、*、/的情况。函数DoOperation接受一个运算符c为输入参数,c为字符类型。此函数首先从操作数堆栈中弹出两个操作数op1和op2,若栈中操作数不足两个,则输出“Missingoperand”信息,表示无法执行运算符c的双目运算。若已经从堆栈中正确获取两个操作数,则执行运算op2<c>op1,并将计算结果进栈,这里,<c>表示运算符c所代表的运算。函数Eval扫描字符数组exp中的后缀表达式,若当前项c是操作数,则进栈;若当前项c是运算符,则调用DoOperation执行运算符c所指定的运算,并将结果进栈。整个扫描处理过程以遇到#号结束。#号作为后缀表达式结束标记。需要提请注意的是本算法对后缀表达式没有错误检测功能,算法要求数组exp中存放的后缀表达式必须是合法的。主函数main引起后缀表达式求值算法Eval的执行。程序中使用了Clear(s)的函数调用,这意味着在表达式计算应用中,还需要增加一个栈运算voidClear(Stack*s),用于清除栈中元素。程序3-5后缀表达式求值voidDoOperation(charc,Stack*s){/*设<c>是运算符c代表的运算,op1和op2是从栈顶依次弹出两操作数,执行运算op2<c>op2,并将运算结果进栈。*/BOOLresult=TRUE;intop1,op2;if(!IsEmpty(*s)){ StackTop(*s,&op1);Pop(s);/*弹出栈顶操作数op1*/}else{ result=FALSE;printf("\nMissingoperand!");}if(!IsEmpty(*s)){ StackTop(*s,&op2);Pop(s);/*弹出栈顶操作数op2*/}else{ result=FALSE;printf("\nMissingoperand!");}if(result)switch(c){/*执行运算op2<c>op1,并将运算结果进栈*/ case'+':Push(s,op2+op1);break; case'-':Push(s,op2-op1);break; case'*':Push(s,op2*op1);break; case'/':if(op1==0.0){ printf("\nDivideby0!");Clear(s);} elsePush(s,op2/op1);break;}elseClear(s);}voidEval(charexp[]){charc;intop;inti;Stacks;CreateStack(&s,Size);for(i=0,c=exp[i];c!='#';i++,c=exp[i]){switch(c){ case'+': case'-': case'*': case'/':DoOperation(c,&s);break; default:Push(&s,c-'0');break; }}if(!IsEmpty(s)){ StackTop(s,&op); printf("\nThereshultis:%d\n",op);}}voidmain(){charexp[ExpSize]={'6','4','2','-','/','3','2','*','+','#'};Eval(exp);}
从上面的讨论可知,无论是将中缀表达式转换成后缀表达式的算法,还是计算后缀表达式的值的算法,都需要借助一个堆栈。对于前者,堆栈中存放运算符(含左括号和#号),所以元素类型为字符型。对于后者,堆栈中存放操作数和中间计算结果,所以堆栈元素类型应为操作数类型(整型、实型等)。为同时实现这两个算法,在C语言下,我们需要设计两个Stack类型模块,它们具有不同的元素类型。另一种可能的方法使我们有可能使用同一个Stack结构类型,使得可在不同元素类型的情况下使用。其做法是:使用C语言提供void指针,将堆栈的元素的类型定义为void指针类型,该类型指针可指向不同具体类型的元素。例如我们可以将堆栈的元素类型定义为:typedefvoid*T;用void指针可实现类属Stack类型。在C++语言环境下,类属ADT可以用C++语言的模板(template)方便地实现。3.4递归和递归过程3.4.1递归的概念递归(recursive)是一个数学概念也是一种有用的程序设计方法。在程序设计中为处理重复性计算最常用的办法是组织迭代循环。除此之外还可以采用递归计算的办法。特别是非数值计算领域中更是如此。递归本质上也是一种循环的程序结构,它把“较复杂”的计算逐次归结为“较简单”的情形的计算,一直归结到“最简单”的情形的计算并得到计算结果为止。许多问题可以采用递归方法来编写程序,一般来说,递归程序结构简洁而清晰,易于分析。数据结构也可以采用递归方式来定义。线性表、数组、字符串和树等数据结构原则上都可以进行递归定义。
1.递归定义说明递归定义的一个例子是斐波那契级数,它的定义可递归表示成
斐波那契级数产生于十二世纪,但直到十八世纪才由A.De.Moivre提出了它的非递归定义式。从十二世纪到十八世纪期间,人们不得不采用斐波那契级数的递归定义来计算。计算斐波那契级数的直接计算公式为
2.递归算法根据斐波那契级数的递归定义可以很自然地写出计算Fh的递归算法。为便于在表达式中直接引用,我们把它设计成一个函数过程,见程序3-6。程序3-6计算斐波那契级数longFib(longn){ if(n<=1)returnn; elsereturnFib(n-2)+Fib(n-1);}
函数Fib(n)中又调用了函数Fib(n-1)和Fib(n-2)。这种在函数体内调用自己的做法称为递归调用,包含递归调用的函数称为递归函数。从实现方法上讲,递归调用与调用其它函数没有什么两样。设有一个函数P,它调用函数Q(x),P被称为调用函数(callingfunction),而Q称为被调函数(calledfunction)。在调用函数P中,使用Q(a)来引起被调函数Q的执行,这里a是实在参数(actualparameter),x称为形式参数(formalparameter)。当被调函数是P本身时,P是递归函数。有时,递归调用还可以是间接的。对于间接递归调用,在这里不作进一步讨论。
3.递归数据结构数据结构原则上都可以采用递归的方法来定义。但是习惯上,许多数据结构并不采用递归方式,而是直接定义,如线性表、字符串和一维数组等。其原因是这些数据结构的直接定义方式更自然、更直截了当。对于第5、6章中将要讨论的广义表和树,通常给出是它们的递归定义。使用递归方式定义的数据结构常称为递归数据结构。3.4.2递归的实现
1.递归函数实现和系统栈递归算法的优点是明显的:程序结构简洁而清晰,且易于分析,因而许多高级语言都提供了递归机制。但递归函数也有明显缺点,它往往既费时又费空间。首先,系统实现递归需要有一个系统栈(systemstack),用于在程序运行时处理函数调用。系统栈是一块特殊的存储区。当一个函数被调用时,系统创建一个活动记录(activationrecord)也称栈幀(stackframe),并将其置于栈顶。
所以,系统栈的元素是栈幀。当一个函数调用另一个函数时,调用函数的局部变量、参数将加到它的栈幀中。一旦一个函数运行结束,将从栈顶弹出调用函数的活动记录,其中保存着被中断的调用函数的运行数据,以及程序中断的位置(返回地址),使得调用函数的程序从中断处恢复执行。假定main函数调用函数a1,图3-9(a)表明了调用之前的系统栈,而图3-9(b)是a1被调用之后的系统栈。由此可见递归的实现是费空间的。
其次,递归是费时的。除了上面提到的当函数递归调用发生时,调用函数的局部变量、形式参数和返回地址需进栈,返回时需出栈,递归过程中的重复计算也是费时的主要原因。我们用所谓的递归树(recursivetree)来描述函数Fib执行时的调用关系。假定在主函数main中调用Fib(4),让我们来看Fib(4)的执行过程。这一过程可以用图3-10所示的递归树描述。从图中可见,Fib(4)需分别调用Fib(2)和Fib(3),Fib(2)又分别调用Fib(0)和Fib(1),...。其中Fib(0)被调用了两次,Fib(1)被调用了三次,Fib(2)被调用了两次。所以许多计算工作是重复的,当然这是费时的。图3-9系统栈示意图(a) main函数;(b)用al图3-10执行Fib(4)的递归树
正是因为递归算法的上述缺点,如果可能,常常愿意将递归改为非递归,即采用循环方法来解决同一问题。如果一个递归函数的递归调用语句是递归函数的最后一句可执行语句,则称这样的递归为尾递归(tailrecursion)。尾递归函数可以容易地改为迭代函数(iterationfunction)。因为当递归调用返回时,总是返回上一层递归调用语句的下一句语句处,在尾递归的情况下,正好返回函数的末尾,因此不再需要利用栈来保存返回地址。此外,除了返回值和引用值外,其它的参数和局部变量值都不再需要,因此可以不用栈,直接用循环形式得到非递归函数,从而提高程序的执行效率。
下面用一个例子来说明这一问题。程序3-7所示的递归函数RPrintArray完成以下标从n到0的次序输出有n+1个元素的一维整数数组list中的所有元素。递归函数RPrintArray的功能不难理解。既然RPrintArray(list,n)的功能是实现依次显示数组元素(list[n],list[n-1],...,list[0]),若n(0,则函数执行语句:
printf("%d",list[n]);RPrintArray(list,--n);
第一个语句显示数组元素list[n],第二个语句是递归调用,它实现显示list[n-1],...,list[0]的功能,所以两个语句实现了对数组元素(list[n],list[n-1],...,list[0])的显示。程序3-8的PrintArray函数是其对应的非递归函数。程序3-7是尾递归程序,消去尾递归很简单,只需首先计算新的n值,n=n-1,即--n,然后程序转到函数的开始处执行就行了,可以使用while语句来实现。【程序3-7】输出数组元素的递归函数。voidRPrintArray(intlist[],intn){if(n>=0){printf("%d",list[n]);;RPrintArray(list,--n);}}函数执行语句:
printf("%d",list[n-1]);RPrintArray(list,--n);【程序3-8】程序3-7的迭代函数。voidPrintArray(intlist[],intn){ while(n>=0){ printf("%d",list[n]); --n; }}程序3-7是尾递归程序,消去尾递归很简单,只需首先计算新的n值,n=n-1,即--n,然后将程序转到函数的开始处执行就行了,可以使用while语句来实现。由上面的讨论可知,编译程序利用栈实现函数的递归调用。事实上,系统栈也是实现一般函数嵌套调用的基础。*3.5演示和测试
从前面的学习可知,在一个数据结构上通常都定义一组运算,一个ADT是数据和运算的集合。实现一个数据结构后,我们需要通过程序测试来确认其正确性。前面,我们通过使用main函数调用各数据结构上定义的运算(函数)来完成这一点。另一种更灵活的方法是设计一个简单的菜单驱动演示程序(menu-drivendemonstrationprogram)来测试数据结构的运算。程序3-9是一个用于测试队列运算的菜单驱动演示程序。演示程序由三个函数组成:GetCommand、DoCommand和main。函数GetCommand提供命令菜单让用户选择所需命令,它返回代表该命令的字符。
函数DoCommand以用户选中的命令为参数,对队列q执行相应的运算。主函数main首先定义和构造一个空队列q,然后使用无限的while循环让用户重复选择命令(即队列运算)和执行相应的运算,直到用户选择命令q终止程序运行为止。值得注意的是,队列q是函数DoCommand的引用参数,这是因为我们希望保存每次队列运算的结果,因此,队列在函数DoCommand中被修改后的状态应返回主函数。
此演示测试程序的做法同样适用于测试堆栈数据结构和以后各章节中讨论的数据结构。读者可以稍加改动用于测试其他数据结构ADT的实现。程序3-9可直接用于测试链式队列而无需作任何改动,只要队列的链接实现严格地符合其ADT的定义就可以了。【程序3-9】菜单驱动队列演示测试程序。#include"queue.h"charGetCommand(void);voidDoCommand(char,Queue*);voidmain(void){Queueq;CreateQueue(&q,20);while(TRUE)DoCommand(GetCommand(),&q);}charGetCommand(void){charcommand;printf("\n\t[I]:Append\t[D]:Serve\t[R]:QueueFront\n" "\t[E]:IsEmpty\t[F]:IsFull\t[P]:QueuePrint\n" "\t[H]:Help\t[Q]:Quit\n" "Selectcommandandpress<Enter>:");while(TRUE){while((command=getchar())=='\n');command=tolower(command);if(command=='i'||command=='d'||command=='r'||command=='e'||command=='f'||command=='p'||command=='q'){while(getchar()!='\n');returncommand;}printf("PleaseenteravalidcommandorHforhelp:");}}voidDoCommand(charcommand,Queue*q){Tx;switch(command){case'i':if(IsFull(*q))printf("Sorry,sisfull.");else{printf("Enternewkey(s)toinsert");x=*InputElement();Append(q,x);}break;case'd'
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2026天津医科大学眼科医院第三批招聘1人笔试备考题库及答案详解
- 2026年芜湖市鸠江区机关事业单位就业见习人员招募笔试备考试题及答案详解
- 2026年息县中小学幼儿园教师招聘考试备考题库及答案解析
- 遂宁市公安局经济技术开发区分局公开招聘10名警务辅助人员笔试备考题库及答案详解
- 2026曲靖市麒麟区卫健系统更正公开引进“珠源百人”医疗卫生人才笔试参考题库及答案详解
- 2026年双柏县网格员招聘考试备考试题及答案解析
- 2026西北工业大学航天学院空间操作技术研究所招聘科研助理1人考试备考试题及答案详解
- 2026广东广州万顷沙人力资源有限公司招聘1人笔试参考题库及答案详解
- 荸荠病虫害防治和治疗办法
- 有关督查督办的试题及精准答案
- 新湘教版九年级上册数学教案(全册)
- 建筑地基处理技术规范DBJ-T 15-38-2019
- 化工装置开车前安全检查
- 国企招聘中层干部笔试题库
- 医院院内感染培训
- 驾照体检表完整版本
- 植物学试题和答案
- GB/T 5202-1985α,β和α-β表面污染测量仪与监测仪
- 关于春节放假的通知范文(关于春节放假的通知范本)
- 特种设备安全培训教材
- 孝道与感恩企业培训教材课件
评论
0/150
提交评论