• 沒有找到結果。

数据结构(C语言版) - 万水书苑-出版资源网

N/A
N/A
Protected

Academic year: 2021

Share "数据结构(C语言版) - 万水书苑-出版资源网"

Copied!
33
0
0

加載中.... (立即查看全文)

全文

(1)第 3 章 栈和队列. 本章学习目标 本章主要介绍两种特殊的线性表—栈和队列的基本概念,存储结构和有关 的常用算法以及递归程序设计的特点、递归程序的执行过程。通过本章的学习,应 熟练掌握以下内容:  了解栈和队列的相关概念  掌握栈和队列的各种存储结构及基本运算的实现  灵活运用栈和队列设计复杂的算法  掌握递归程序设计的特点及了解递归程序的执行过程 栈和队列是两种特殊的线性表,它们的逻辑结构和线性表相同,只是其运算规则较线性 表有更多的限制,故又称它们为运算受限的线性表。栈和队列被广泛应用于各种程序设计中。 本章介绍它们的结构和应用。. 3.1. 栈. 栈,也叫堆栈,是最常用也是最重要的数据结构之一。比如编译器中的语法识别、数学 表达式的处理、程序运行中的函数及过程的调用等,都要用到栈的有关特性。它们是栈应用于 实际问题的典型。 3.1.1 栈的定义和运算 1.栈的定义 栈可以看成是一种特殊的线性表,插入或删除栈元素的运算只能在表的一端进行,称运 算的一端为栈顶,另一端称为栈底。这样一来,压栈越早的元素,离栈顶就越远,压栈越晚的 元素离栈顶越近,最后压栈的元素恰好在栈顶。于是,后压栈的元素先出,先压栈的元素后出。 栈顶的位置在插入和删除的运算过程中不断变化,而栈底的位置保持不变。 为了加深对栈的理解,通过一个生活中的例子说明栈结构的特征。如设某餐馆有一个能 放若干个盘子的容器,但每次只容许一个盘子进出容器。当服务人员洗净一个盘子之后,就放 在一摞盘子的最上面(相当于压栈)。当需要取一个盘子用的时候,就从这摞盘子最上面取出 一个(相当于出栈),这是很自然的事情。例:现有 5 个盘子分别编号为①~⑤,按编号的顺 序依次进入此容器,如图 3-1 所示。此时若要取出编号为④的盘子,必须等取出⑤之后才可以。 若要取出①,则必须等到⑤④③②依次都取出后,①才能取出来。这里,盘子进出容器的过程 是按照后进先出的原则。.

(2) 46. 数据结构(C 语言版) 出. 进. 容器口. ⑤. 栈顶. ④ ③ ② 容器底. 图 3-1. ①. 栈底. 盘子进出容器示意图. 放这摞盘子的容器可以比作栈,每个盘子就是一个数据元素。栈顶相当于容器口,栈底 相当于容器的另一端。进、出容器可看作栈的插入、删除运算。插入、删除都在栈顶进行,相 当于进出都经过容器口。这表明利用栈来运算数据元素的特点是后进先出。因此,栈又称为“后 进先出”的线性表,简称 LIFO 表。意思是说后放进去的数据先取出来用,而不能抽取较早放 入的数据。 2.栈的基本运算 栈有两个主要的运算:插入和删除。在栈顶进行插入一个元素的运算,被称为压栈(Push)。 在栈顶进行删除一个元素的运算,被称为出栈(Pop)。除此之外,还有栈的初始化、判定某 个栈是否为空栈、读取栈顶元素等。栈的基本运算具体内容如下: (1)初始化栈 InitStack(S):其作用是设置一个空栈 S。 (2)压栈 Push(S,x):其作用是将元素 x 插入栈 S 中,使 x 成为栈 S 的栈顶元素。 (3)出栈 Pop(S,x):其作用是当栈 S 不空时,由 x 返回栈顶元素,并从栈中删除栈顶 元素。 (4)取栈顶元素 GetTop(S,x):若栈 S 不空,则由 x 返回栈顶元素。 (5)判栈空 Empty(S):若栈 S 为空栈,结果为 1,否则结果为 0。 3.1.2 栈的顺序存储结构及其基本运算的实现 和线性表类似,栈也有两种存储表示方法,即顺序存储和链式存储。 1.栈的顺序存储结构 栈的顺序存储结构简称为顺序栈。通常由一个一维数组和一个记录栈顶元素位置的变量 组成。习惯上将栈底放在数组下标小的那端。假设用一维数组 S[MaxSize](下标 0~MaxSize-1) 表示一个栈,MaxSize 为栈中可存储数据元素的最大个数,即栈的最大长度。栈顶位置可用一 个整型变量 top 记录当前栈顶元素的下标值。 当 top 指向-1 时,表示栈空,当 top 指向 MaxSize-1 时,表示栈满。如图 3-2 所示为顺序栈最大长度 MaxSize 为 4 时的几种状态。 (a)当栈中没有数据元素时,表示顺序栈为栈空。这也是初始化运算得到的结果。此时 栈顶下标值 top=-1。如果再要进行出栈运算,则产生“下溢”。 (b)表示栈中只含一个元素 A,在(a)的基础上用压栈运算 Push(S, 'A'),可以得到这种.

(3) 第 3 章 栈和队列. 47. 状态。此时栈顶元素所对应的下标值 top=0。 3. 3 top. D. 3. 2. 2. C. 2. 1. 1. B. 0. A. 0 top. top. A. 3. 3. C. 2. 2. 1. B. 1. 1. 0. A. 0. 0. top. top. -1. (a). (b). (c) 图 3-2. (d). -1. (e). 顺序栈的几种状态. (c)表示在(b)基础上又有 3 个元素 B、C、D 先后入栈,此时栈顶元素的下标值 top=3。 栈中已放满了元素,表示顺序栈为栈满。若再有数据元素进栈,栈将溢出,称为“上溢”。 (d)表示在(c)状态下,执行一次 Pop(S,x)运算得到。此时栈顶元素下标值 top=2,故 C 为当前的栈顶元素。 (e)表示在(d)状态下,执行 3 次 Pop(S,x)运算得到。此时栈顶下标值 top=-l,又变成 栈空状态。 顺序栈的 C 语言描述如下: #define MaxSize <存储数据元素的最大个数> typedef struct { ElemType data[MaxSize]; int top; }STACK;. 顺序栈被定义为一个结构体类型,它有两个域:data 和 top。data 为一个一维数组,用于 存储栈中元素。ElemType 为栈中元素的数据类型,可以根据需要而指定为某种具体的类型。 top 为 int 型,它的实际取值范围为 0~MaxSize-1,top=-1 表示栈空,top=MaxSize-1 表示栈满。 2.基本运算在顺序存储结构的实现 (1)初始化栈。 void InitStack(STACK *S) //对顺序栈 S 进行初始化,即将 S 置为一空栈 { S->top=-1; }. (2)压栈。 int Push(STACK *S,ElemType x) { //将一个新数据元素 x 插入到当前 S 的栈顶 if(S->top==MaxSize-1){ printf("\n Stack is full!"); return 0; } S->top++; S->data[S->top]=x; return 1; }. (3)判断栈空。.

(4) 数据结构(C 语言版). 48. int Empty(STACK *S) //判断顺序栈 S 是否为空 { return (S->top==-1?1:0);}. (4)出栈。 int Pop(STACK *S,ElemType *x) {//删除顺序栈 S 的栈顶元素,并用 x 返回栈顶元素 if(Empty(S)){ printf("\n Stack is free!"); return 0; } *x=S->data[S->top]; S->top--; return 1; }. (5)取栈顶元素。 int GetTop(STACK *S, ElemType *x) {//取当前 S 栈顶元素的值, 并用 x 返回栈顶元素 if(Empty(S)){ printf("\n Stack is free!"); retrn 0; } *x=S->data[S->top]; return 1; }. 【例 3-1】应用栈的数据结构解决数制转换问题。 【解】将一个非负的十进制整数 N 转换为另一个等价的基数为 B 的 B 进制数,可以通过 “除 B 取余法”来解决。 例如:将十进制数 13 转化为二进制数。按除 2 取余法,得到的余数依次是 1,0,1,l, 则转化后对应的二进制数为 110l。由于最先得到的余数是转化结果的最低位,最后得到的余数 是转化结果的最高位,所以可以把十进制数每次除以 2 所得到的余数顺序放到一个顺序栈中, 直到商为 0 时,再将顺序栈中的元素依次出栈,就得到了对应的二进制数。此运算过程可采用 栈的数据结构来解决。 以下函数给出了十进制数转化成其他进制数的算法。 #define MaxSize 100 //顺序栈初始分配空间 typedef int ElemType; typedef struct{ ElemType data[MaxSize]; int top; }STACK; char B[]= "0123456789ABCDEF"; void DecToOthers(int n,int b) {//将非负的十进整数 n, 输出等值的 b 进制数 int x; STACK st; InitStack(&st);.

(5) 第 3 章 栈和队列. 49. while(n){ Push(&st,n%b); n=n/b; } while(!Empty(&st)){ Pop(&st,&x); printf(“%c”,B[x]); } }. 若调用函数 DecToOthers(13,2),则输出序列 1101。 3.栈的共享存储单元 对于出栈运算中的“下溢” ,在实际应用中,可用来作为控制程序转移的判别标志,是十 分有用的。对于压栈算法中的“上溢”,则是一种致命的错误,将使程序无法继续运行,所以 要设法避免。对于一般实际问题的栈,它的长度往往不是很大,所以避免出现“上溢”的方法 是将栈的容量加到足够大。为了缓解栈满的矛盾,在一个程序中使用多个栈时,当一个栈发生 “上溢”时,其他栈可能还留有很多空间。可以让多个栈共享一个数组,根据栈的动态变化灵 活分配使用空间,最大限度地利用栈空间。下面仅以两个栈为例,来讨论共享内存时的顺序分 配方法。 当有两个栈共享大小为 m 的内存空间时,可以把两个栈的栈底分别设在给定内存空间的 两端。它们的栈顶各自向中间伸展,仅当两个栈顶相遇时才产生溢出。使用这种分配方法,在 每个栈的动态变化过程中,任何一个栈可利用的最大空间均有可能超过 m/2。这不仅增加了内 存空间的利用率,而且也减少每个栈溢出的可能性,因此,这种方案经常被采用。图 3-3 是这 种分配方法的示意图。. 栈1. top1. 图 3-3. top2. 两个栈共享内存空间. 两个栈的共享存储单元可用 C 语言描述如下: #define MaxSize <共享存储单元的最大长度> typedef struct{ ElemType data[MaxSize]; int top[2]; }STACK;. 栈共享存储单元的压栈,出栈算法如下: (1)两个栈共享存储单元的压栈算法。 int Push(STACK *S,ElemType x,int k) //将元素 x 压入到以 S 为栈空间的第 k 个栈中 { if(S->top[0]+1==S->top[1]){ printf("\n stack is full!"); return 0; }. 栈2.

(6) 数据结构(C 语言版). 50 if(k==0){ S->top[0]++; S->data[S->top[0]]=x; } else{ S->top[1]--; S->data[S->top[1]]=x; } return 1; }. (2)两个栈共享存储单元的出栈算法。 int Pop(STACK *S,int k,ElemType *x) //将以 S 为栈空间的第 k 个栈中的栈顶元素出栈 { if((k==0)&&(S->top[0]==-1)){ printf("\n stack is free!"); return 0; } if((k==1)&&(S->top[1]==MaxSize)){ printf("\n stack is free!"); return 0; } if(k==0){ *x=S->data[S->top[0]]; S->top[0]--; } else{ *x=S->data[S->top[1]]; S->top[1]++; } return 1; }. 不论采取何种措施,栈满的问题都不能绝对避免。为了从根本上解决这个问题,最好使 用链式存储结构。. 3.1.3. 栈的链式存储结构及其基本运算的实现. 1.栈的链式存储结构 栈的链式实现是以链表作为栈的存储结构,并在这种存储结构上实现栈的基本运算。栈 的链式实现称为链栈,其组织形式与单链表类似,但其运算受限制,插入和删除运算只能在链 栈的栈顶进行。栈顶指针也就是链表的头指针,如图 3-4 所示。为了简便算法,链栈带一表头 结点,表头结点后面的第一个结点就是链栈栈顶结点,top 称为栈顶指针,它惟一地确定一个 链栈。栈中的其他结点通过它们的 next 域链接起来,栈底结点的 next 域为 NULL。 链栈的 C 语言描述如下: typedef struct snode { //定义链栈结点类型 ElemType data;.

(7) 第 3 章 栈和队列. 51. struct snode *next; }LinkSTACK; LinkSTACK *top; data next top. A. B. C. 栈顶. 图 3-4. ^. 栈底. 链栈结构示意图. 图 3-5 展示了链栈的几种状态。 top. B. top ^. (a)top->next=NULL 表示空栈. A. A. ^. ^. (b)A,B 两个元素顺序入栈 图 3-5. top. (c)B 元素出栈. 链栈的几种状态. 2.基本运算在链式存储结构上的实现 下面通过对链栈存储结构的描述来讨论链栈的基本运算。因为链栈中的结点是动态生成 的,所以在用户内存空间的范围内不会出现栈满情况,因而可以不考虑栈满的运算,其他运算 的算法描述如下: (1)栈初始化。 void InitStack(LinkSTACK **top) { *top=(LinkSTACK *)malloc(sizeof(LinkSTACK)); (*top)->next=NULL; }. (2)压栈运算。 int Push(LinkSTACK **top,ElemType x) { LinkSTACK *s; s=(LinkSTACK *)malloc(sizeof(LinkSTACK)); s->data=x; s->next=(*top)->next;.

(8) 数据结构(C 语言版). 52 (*top)->next=s; return 1; }. (3)判断栈空。 int Empty(LinkSTACK **top) { return ((*top)->next==NULL?1:0);}. (4)出栈运算。 int Pop(LinkSTACK **top,ElemType *x) { LinkSTACK *s; if(Empty(top)){ printf("\n stack is free!"); return 0; } s=(*top)->next; *x=s->data; (*top)->next=s->next; free(s); return 1; }. (5)取栈顶元素。 int GetTop(LinkSTACK **top,ElemType *x) { if(Empty(top)){ printf("\n stack is free!"); return 0; } *x=(*top)->next->data; return 1; }. 3.2. 栈的应用. 本节讨论表达式求值问题。在程序设计中经常要用表达式进行求值,如何将它们处理成 能够正确求值的指令序列,是编译系统要解决的一个最基本的问题,它的实现是栈应用的一个 典型例子。 对任意一个表达式,都可以将其分解成 3 个部分:操作数、操作符和分界符。为了讨论 方便,此处的操作数使用常数。操作符仅限于+、-、×、/、%等算术运算符。分界符指左右 括号和表达式结束符等,其他情况可依此类推。 表达式的表示形式有中缀、前缀和后缀 3 种形式。中缀表达式是操作符处于两个操作数 之间的表达式,通常人们使用的就是这种形式。前缀和后缀表达式分别是指操作数处于两个操 作数之前和之后的表达式。 3.2.1 中缀表达式 中缀表达式是常用的表达式之一。通常使用简单直观的“算符优先法”求解中缀表达式.

(9) 第 3 章 栈和队列. 53. 的值。 要把一个表达式翻译成正确求值的机器指令序列,或者直接对表达式求值,首先要能够 正确解释表达式。例如,对下面的算术表达式求值: 4×(6-4)+10/5 首先要了解算术四则运算的规则,即: (1)对带括号的操作执行顺序为:先括号内,后括号外。如遇多层括号,则按从最内层 依序至外的操作顺序。 (2)先乘除,后加减。 (3)优先级相同的操作符的运算遵循左结合律,即按操作符先后次序从左到右。 根据运算的优先性,上述算术表达式的计算顺序为:先求括号内 6-4 得 2,然后求 4×2 得 8,再求 10/5 得 2,最后求 8+2 得 10。 “算符优先法”就是根据对运算优先关系的规定来实现对表达式的编译或解释执行的。 具体实现是通过使用两个栈,一个用于暂存操作数和运算结果,另一个用于暂存操作符。为了 运算方便,对于所给的任一表达式,在其首尾各增加一个“#”作为表达式的开始和结束标志 符。各操作符的优先级规定如表 3-1 所示。 表 3-1 操作符的优先级 操作符. #. (. +. -. *. /. %. ). 栈内优先级. 0. 1. 3. 3. 5. 5. 5. 7. 栈外优先级. 0. 6. 2. 2. 4. 4. 4. 1. 为了体现同级运算符(如“+”和“-”或“×”和“/”)从左到右的运算次序,定义已经 进栈的操作符的优先级比在栈外等待的操作符的优先级高 1。在下面的讨论中,假定输入的表 达式不会出现语法错误。 算法的基本思想是: (1)操作数栈置为空栈,表达式起始符“#”为运算符栈的栈底元素。 (2)从左到右扫描表达式,依次读入表达式中的每个字符。若所读字符为“#” ,且操作 符的栈顶元素也为“#”,则输出操作数栈中的栈顶数据,即为运算的结果,结束处理。否则, 进行下面的处理。 (3)若为操作数,入操作数栈;若为操作符,则要将当前操作符和操作符栈中的栈顶操 作符进行优先级比较。 1)如果当前操作符的优先级大于栈顶操作符的优先级,则将当前操作符压入操作符栈中; 转第(2)步。 2)如果当前操作符的优先级等于栈顶操作符的优先级,则将当前操作符栈顶的操作符出 栈。转第(2)步。 3)如果当前操作符的优先级小于栈顶操作符的优先级,将当前操作符栈顶的操作符出 栈,并从操作数栈中顺次出栈两个操作数(先退出作为运算符的右操作数,后退出作为运 算符的左操作数)。用刚出栈的操作符对两个操作数进行计算求值,将所得值压入操作数栈 中。转第(3)步。.

(10) 54. 数据结构(C 语言版). 以下函数描述了这个求值过程: char OP[]="#(+-×/%)"; #define MaxSize 40 //运算数栈的描述 typedef double DElemType; typedef struct{ DElemType data[MaxSize]; int top; }OPND_STACK; //运算符栈的描述 typedef char RElemType; typedef struct{ RElemType data[MaxSize]; int top; }OPTR_STACK; double cal_expression(char *exp) //求表达式函数;表达式存储在字符串 exp 中 { OPND_STACK OPND; //设 OPTR 和 0PND 分别为运算符栈和运算数栈 OPTR_STACK OPTR; char ch,theta; double x,a,b; int pos=0; InitStack_R(&OPTR); Push_R(&OPTR,'#'); //初始化运算符栈 InitStack_D(&OPND); //初始化运算数栈 GetTop_R(&OPTR,&ch); //取运算符栈栈顶元素 while(exp[pos]!='#'||ch!='#'){ //两个条件同时成立结束 if(strchr(OP,exp[pos])==NULL){ //不是运算符则进数栈 //当前字符位置截取浮点型字符串,并转化成相应实数 x=GetFloat(exp,&pos); Push_D(&OPND,x); } else{ //是运算符 //比较栈顶算符 ch 和当前算符的优先级 switch(Precede(ch,exp[pos])){ case '<': Push_R(&OPTR,exp[pos]);//栈顶算符优先权低 pos++;break; case '=': Pop_R(&OPTR,&ch); //脱括号并准备接收下一字符 pos++;break; case '>': Pop_R(&OPTR,&theta);//退栈并将运算结果入栈 Pop_D(&OPND,&b);Pop_D(&OPND,&a); Push_D(&OPND,Operate(a,theta,b)); break; } //switch } GetTop_R(&OPTR,&ch); } //while GetTop_D(&OPND,&x); return x; } main().

(11) 第 3 章 栈和队列 { char expression[80]; printf("\n 请输入表达式,以#结束输入!"); gets(expression); printf("\n%lf",cal_expression(expression)); }. 算法中其他函数的实现方法: char Precede(char oper1,char oper2) {//判定运算符栈的栈顶运算符 oper1,与当前运算符 oper2 之间的优先关系 int grade; // 取得栈顶运算符与当前运算符的优先值 grade=Get_PRI(1,opre1)-Get_PRI(0,oper2); if(grade>0) return '>'; else if(grade==0) return '='; else return '<'; } Get_PRI(int mode,char oper) {//返回运算符 oper 代表优先级的整数值;mode 为 1 ,表示 oper 是栈顶运算 //符;否则为当前运算符 switch(oper){ case '#': return 0; case '(': return(mode?1:6); case '+': case '-': return(mode? 3: 2); case '×': case '%': case '/': return(mode? 5: 4); case ')': return(mode ? 7:1); } } double Operate(double a,char theta,double b) {//Operate 为进行二元运算 a theta b 的函数; 并返回运算的结果 double y; switch(theta){ case '+': y=a+b;break; case '-': y=a-b;break; case '×': y=a*b;break; case '/': y=a/b;break; } return y; } double GetFloat(char *exp,int *pos) { int i=0; char str[40]; while(strcat(OP,exp[*pos])==NULL){ //当前位置不是运算符 str[i++]= exp[*pos]; (*pos)++; }. 55.

(12) 数据结构(C 语言版). 56. str[i]=0; return StrToFloat(str); } double StrToFloat(char *str) {//将一数字字符和圆点组成的字符序列转换为实数 int i=0; double x=0,y=0; double j=10.0; //j 作为相应小数位的权值 while(str[i]>='0' && str[i]<='9'){ //x 保留扫描到的整数部分值 x=x*10+(str[i]-'0'); i++; } if(str[i]=='.'){ //y 保留扫描到的小数部分值 i++; while(str[i]>='0' && str[i]<='9'){ y+=(str[i]-'0')/j; j*=10; i++; } } x+=y; return x; }. 图 3-6 展示了利用以上算法对算术表达式 3×(7-2)求值的动态过程。 3*( 7-2)# *(7-2)#. (7-2)#. 7-2)#. -2)#. 2)#. 运 算 数 栈. )#. )#. #. #. 2. 3. 3. 运 算 符 栈. 3. 7. 7. 7. 5. 5. 3. 3. 3. 3. 3. -. -. (. (. (. (. (. *. *. *. *. *. *. *. #. #. #. #. #. #. #. #. #. (a). (b). (c). (d). (e). (f). (g). (h). (i). 图 3-6. 表达式求值过程栈的动态变化示意图. 15. #. (j).

(13) 第 3 章 栈和队列. 57. (a)运算数和运算符栈初始化后的状态。 (b)执行 Push_D(&OPND,3)后的状态。 (c)执行 Push_R(&OPTR, '*')后的状态。 (d)执行 Push_R(&OPTR, ' (')后的状态。 (e)执行 Push_D(&OPND,7)后的状态。 (f)执行 Push_R(&OPTR, '-')后的状态。 (g)执行 Push_D(&OPND,2)后的状态。 (h)执行 operate(7, '-',2)后的状态。 (i)执行 Pop_R(&OPTR,&ch)后的状态。 (j)执行 operate(3, '*',5)后的状态。 3.2.2 中缀表达式转换为等价的后缀表达式 从上例可以看出,中缀表达式求值较麻烦,需用两个栈来实现。如果把中缀表达式转换 成一个与之等价的后缀表达式(也称为逆波兰式),可使表达式的求值过程既简单又便于计算 机处理。等价是指两个表达式的计算顺序和计算结果完全相同。 中缀表达式 等价后缀表达式 0.3/(5×2+1) 0.3 5 2×1+/ 16-9×(4+3) 16 9 4 3+×2×(x+y)/(1-x) 2 x y +×1 x-/ (25+x) ×(a×(a+b)+b) 25 x+a a b+×b+× 仔细观察以上等价的表达式可知,由中缀表达式转换为后缀表达式的规则是:转换前后, 操作数的出现次序不变,但运算符的出现次序不同,由处在两个操作数中间变为跟在两个操作 数之后,同时去掉所有的括号。在后缀表达式中,运算符的出现次序是实际进行操作的次序。 在中缀表达式中,由于受到操作符的优先级和括号的影响,操作符的出现次序与实际操作的次 序可能不一致。 为了保证转换正确,必须设定一个运算符栈,并在栈底放入一个特殊运算符,假定为“#” 字符。让它具有最低的运算符优先级,可设为数值 0。此栈用来保存扫描中缀表达式得到的且 暂不能放入后缀表达式中的运算符。待它的两个运算对象都放入后缀表达式以后,再令其出栈 并写入后缀表达式中。 设中缀算术表达式已经保存在 s1 字符串中,转换后得到的后缀算术表达式拟存于 s2 字符 串中。把中缀表达式转换为后缀表达式算法的基本思路是:程序从左到右扫描中缀表达式,若 遇到数字或小数点,直接写入 s2 中,并在每个数值的最后写入一个空格作分隔符。遇到运算 符时,必须与栈顶运算符进行比较,若运算符级别比栈顶的级别高就进栈,否则退出栈顶元素 并写入 s2 中。若遇到左括号,则应把它压入运算符栈中,直到以它开始的括号内的表达式全 部转换完毕再出栈;若遇到右括号,则表明括号内的中缀表达式已经扫描完毕,则一直退栈, 并把退栈的运算符依次写入 s2 串中,直到退到左括号止。 重复上述步骤直到遇到中缀表达式的结束标记“#”,再把栈中剩余的运算符依次退栈并 写入 s2 中,最后向 s2 写入字符串结束符‘\0’,整个转换过程处理完毕。在 s2 串中就得到了转 换成的后缀表达式。.

(14) 数据结构(C 语言版). 58. 具体算法描述如下: void postfix(char *s1,char *s2) { //定义暂存运算符的栈 OPTR,该栈的元素类型为 char STACK OPTR; int i=0,j=0; char ch,oper; InitStack_R(&OPTR); Push_R(&OPTR,'#'); GetTop_R(&OPTR,&ch) while(s1[i]!='#'||ch!='#'){ //两个条件同时成立结束 if(strcat(OP,s1[i])==NULL){ //是操作数直接输出 s2[j++]=s1[i++]; } else{ s2[j++]=' '; GetTop_R(&OPTR,&oper); swithch(Precede(oper,s1[i])){ case '>':Push_R(&OPTR,s1[i]); //栈顶算符优先级高 i++;break; case '=':Pop_R(&OPTR,&oper); //脱括号并接收下一字符 i++;break; case '<':Pop_R(&OPTR,&oper); s2[j++]=oper;break; } } GetTop_R(&OPTR,&ch) } s2[j++]='#';s2[j]=0; }. 3.2.3 后缀表达式及其求值 后缀表达式中只有操作数和操作符,不再含有括号,操作符在两个操作数之后。后缀表 达式的求值比较简单,严格按照从左向右的次序依次扫描一遍即可完成。它需要使用一个栈, 其元素类型应为操作数的类型,假定为双精度型 double。用此栈存储后缀表达式中的操作数、 计算过程中的中间结果以及最后结果。 后缀表达式求值算法的基本思路是:将遇到的操作数暂存于一个操作数栈中,凡是遇到 操作符,便从栈中弹出两个操作数执行相应的操作,并将结果存于操作数栈中,直到处理完后 缀表达式中最后一个操作符,最后压入栈中的数就是后缀表达式的计算结果。 设待求值的后缀表达式是符合文法的。在后缀表达式求值算法中,后缀表达式是以字符 数组的形式存放在—维数组中。表达式的结束标记为“#”,两个操作数之间以一个空格分隔。 如:后缀表达式 3.5 2.5 2.0*+,在数组中的存放形式为: 3. .. 具体算法如下:. 5. 2. .. 5. 2. .. 0. *. +. #.

(15) 第 3 章 栈和队列. 59. double CalPostExpression(char *str) {//计算由 str 所指字串的后缀表达式的值 STACK OPRD; //OPRD 元素类型为 double int pos=0; double a,b,x,exp; InitStack(&OPRD) while(str[pos]!='#') if(str[pos]==' ') pos++; else if(strcat(OP,str[pos])==NULL){ exp=GetFloat(str,&pos); Push_D(&OPRD,exp); } else{ //是运算符 Pop_D(&OPRD,&b); Pop_D(&OPRD,&a); Push_R(&OPRD,operate(a,str[pos],b)); pos++; } Pop_D(&OPRD,&x); return x; }. 3.3. 栈与递归. 递归是一个非常重要的数学概念和解决问题的方法,在计算机科学和数学等领域中有着 广泛的应用。在计算机科学中,许多数据结构,如广义表、树和二叉树等,由于其自身固有的 递归性质,都可通过递归方式加以定义并实现许多问题的算法设计。在计算机内部,通过栈来 实现递归算法,所以递归是栈的一个实际应用。下面详细介绍这方面的内容。 3.3.1 递归与递归程序的设计 递归就是某一事物直接或间接地由自己组成。具体说来,是把求解一个复杂的大问题逐 层分解成若干子问题,通过使用同样的求解方法先解决最小的问题,再依次返回解决较大问题 最终解决整个问题。解决递归问题的算法称为递归算法。用这种方式调用的函数称为递归函数。 递归函数的执行过程实际上是一个函数嵌套调用过程。在递归调用中,就是根据递归条件直接 或间接地调用算法本身,当满足终止条件时结束递归调用。 在 C 的递归函数调用中,最先被调用的函数总是最后执行完,最后调用的函数却最先被 执行。当然,对于一些简单的递归问题,也很容易把它转换为循环问题来解决,从而使编写出 的算法更为有效。 下面是一个简单递归程序设计的实例。 【例 3-2】采用递归算法求解正整数 n 的阶乘 n!。 【解】由数学知识可知,n 阶乘的定义是递归的,即 n!=n*(n-1)!,且规定 0 的阶乘为 1。 则有:.

(16) 数据结构(C 语言版). 60. 1. n=0. n*(n-1). n>0. n! = 在此定义中,若要求得 4!,先要求出 3!;要求 3!,先要求出 2! ……依次类推,直到 n=0 时,可以直接得到 0! =1。此时的情况称为递归的终止条件。然后返回上层求 1!=1*0!=1;再往 上返,求 2!=2*1!=2……依次类推,最终求出 4!=4*3!=24。 如果用 fact(n)表示求 n!的阶乘函数,根据上面的递归定义,求 n 的阶乘的递归函数算法 如下: long fact(int n) { long f; if(n==0) f=1; else f=n*fact(n-1); return f; }. 【例 3-3】采用递归算法求解 Fibonacci 级数第 n 项的值。 【解】该级数的求解规律是:级数前两项的值分别为 1,1,从第三项开始,每一项的值 是前两项之和。假设用 fab(n)表示第 n 项 Fibonacci 级数的值,则求解 Fibonacci 级数的递归定 义为:. fib(n)=. 1 1 fib(n-1)+fib(n-2). n=1 n=2 n>2. 具体算法如下: long fib(int n) { int f; if(n==1||n==2) f=1; else f=fib(n-1)+fib(n-2); return f; }. 从上面的实例可以看出,使用递归进行程序设计,必须符合以下 3 个条件: (1)可以把一个问题的求解转化为一个新问题的求解,而新问题的解决方法仍与原问题 的解法相同,只是处理的对象有所不同,但它们仍是有规律地递增或递减的。 (2)可以通过转化过程使问题得到解决。 (3)必须要有一个明确结束递归的条件。 3.3.2 递归程序执行过程的分析 采用递归技术设计出来的程序,具有结构清晰、可读性强、便于理解等优点。但递归 程序的执行过程有时却令人难以理解。由于递归调用是对函数自身的调用,在一次函数调 用未终止之前又开始了另一次函数调用。按照语言关于作用域的规定,函数的执行在终止 之前其所占用的空间是不能回收的,必须保留。这就意味着函数自身每次不同的调用,需 要分配不同的空间。为了对这些空间实施有效的管理,在递归程序的运行过程中,系统内.

(17) 第 3 章 栈和队列. 61. 部设立了一个栈,它是由系统来维护的,对于用户是不可见的,用于存放每次函数调用与 返回所需的各种数据,主要包括函数调用执行完成时的返回地址、函数的返回值、每次函 数调用的实参和局部变量。 每次调用一个递归函数时,其新分配的变量就被压入堆栈。任何对这些局部变量和参数 的引用都是通过堆栈的当前栈顶位置来访问的。当函数返回时,都按照本次返回地址返回到指 定的位置执行,并且自动做一次退栈操作。于是分配在顶层的变量就被释放,使得上一次调用 分配的变量成为新的栈顶,并将其作为局部变量被函数引用。 例如,对于求 n 阶乘的递归函数 fact(n),当调用它时系统自动建立一个栈,该栈中的元素 包含值参 n 的域和返回地址 r 域。假设调用程序包含如下语句: printf("%ld",fact(4)); 调用后的返回地址用 r1 表示。在 fact(n)函数中,每次进行 fact(n-1)调用的返回地址用 r2 表示。则系统所使用栈的数据变化情况如图 3-7 所示。 如图 3-7 所示,每个栈状态的栈顶元素的 n 域是调用 fact(n)函数时为值参 n 所分配的 存储空间,r 域是为保存当前一次调用结束后的返回地址所分配的存储空间。进行 fact(4) 调用时,把新分配的值参 n=4 压入堆栈,所以栈顶元素中的 n 域保存的值为 4,返回地址 域保存的值为 rl。当执行 fact(4)调用结束(即执行到函数体的右花括号结束符)后,就返 回到 r1 的位置执行。又如当执行 fact(3)调用时,栈顶元素中的值参 n 域保存的值为 3,返 回地址域保存的值为 r2。当调用 fact(3)结束后,就返回到 r2 的位置(即上一层返回表达式 中乘号后面的位置)执行。 n. 4. r. r1. n. r. n. r. n. r. n. r. n. r. 0. r2. 1. r2. 1. r2. 1. r2. n. r. n. r. 2. r2. 2. r2. 2. r2. 2. r2. 2. r2. 3. r2. 3. r2. 3. r2. 3. r2. 3. r2. 3. r2. 3. r2. 4. r1. 4. r1. 4. r1. 4. r1. 4. r1. 4. r1. 4. r1. n. r. 4. r1. n. r. 调用 fact(4 ) 调用 fact(3) 调用 fact(2) 调用 fact(1 ) 调用 fact(0) 返回fact(0) 返回 fact(1) 返回 fact(2) 返回fact (3) 返回 fact(4 ). 图 3-7. 进行 fact(4)调用的系统栈的变化情况. 从主函数或其他函数非递归调用 fact(4),这一函数的递归调用过程的执行流程可形象地用 图 3-8 表示。 调用 fact(n)算法时,系统所使用栈的最大深度为 n+l,n 为首次调用时传送来的实参值, 所以其空间复杂度为 O(n)。又因为每执行一次递归调用就是执行一次条件语句,其时间复杂 度为 O(1)。执行整个算法求出 n!的值需要进行 n+1 次调用,所以其时间复杂度也为 O(n)。如 果采用非递归的循环算法求解 n!的问题,其空间复杂度为 O(1),时间复杂度为 O(n),并且省 去进出栈的烦琐操作,显然比采用递归算法更为有效。这里对求 n 阶乘采用递归算法,只是为 了详细说明系统对递归算法的处理过程,以便能够理解更复杂的递归算法。.

(18) 数据结构(C 语言版). 62 n:3. n:4 (1). fact(4). (2). f=4*fact(3); (20). (19). (4). (3). (6) (5). f=3*fact(2); (18). return f;. n:1. n:2. (8). (7). (16). f=1*fact(0);. return f;. (14). (15). (10) (9). f=2*fact(1);. (17). n:0. (13). return f;. return f;. f=1; (12). (11). return f;. 图 3-8 fact(4)的递归调用过程. 3.3.3 递归的应用举例 对于一个比较复杂的应用,如果不使用递归就很难解决。例如 n 阶 Hanoi 塔问题就是一个 典型的递归应用。 n 阶 Hanoi 塔问题是:设有 3 个塔座,依次命名为 X,Y 和 Z。有 n 个直径各不相同的圆 盘,由小到大依次编号为 1,2,…,n。开始时,它们全部按圆盘大小递减的次序插在塔座 X 上, 如图 3-9 所示。现要求把这 n 个圆盘以大小递增的次序插放到塔座 Z 上。移动圆盘时应遵守以 下规则: (1)每次只能移动一个圆盘。 (2)圆盘可以从任一塔座移到另一塔座上。 (3)任何时候都不能把大的圆盘压在小的圆盘之上。 X. Y. Z. 1 2 3. 图 3-9 3 阶 Hanoi 塔问题. 解题的基本思路是:当 n=1 时,是 1 阶 Hanoi 塔问题。只有一个圆盘,此时只要将此圆 盘直接从 X 塔座移到 Z 塔座即可。若 n>1,则进行如下步骤的处理: (1)把 n-1 个盘子从 X 塔搬到 Y 塔。所有 n-1 个盘子是符合要求地从一个塔移到另一个 塔(可借用 Z 塔)。 (2)将剩下的一只盘(最大的一只)直接从 X 塔搬到空着的 Z 塔上。 (3)用方法(1) ,再次将 Y 塔上的盘搬到 Z 塔。和第一步一样,这实际上是由一个序列 更小的一次仅搬一个盘的操作组成。这一步是没有问题的,因为 Z 塔上仅有的一只盘是最大 的盘。.

(19) 第 3 章 栈和队列. 63. 这样就把求解 N 阶 Hanoi 塔问题就转化为求 n-1 阶 Hanoi 塔问题。依此类推,最终转化为 求解 1 阶 Hanoi 塔问题。 利用递归求解 n 阶 Hanoi 塔问题的算法描述如下: void Hanoi(int n,char x,char y,char z) //X 塔座的 n 个圆盘移到 Z 塔座上,Y 为辅助塔座 1{ 2 if(n==1) 3 move(x,1,z); //将 X 塔座上的 1 号盘移到 Z 塔座 4 else{ 5 Hanoi(n-1,x,z,y);//X 塔座的 n-1 个圆盘移到 Y 塔座上,Z 为辅助塔座 6 move(x,n,z); //将 X 塔座上的 n 号盘移到 Z 塔座 7 Hanoi(n-1,y,x,z);//Y 塔座的 n-1 个圆盘移到 Z 塔座上,X 为辅助塔座 8 } 9 }. 以 Hanoi(3,x,y,z)为例来看递归函数执行时工作栈状态的变化情况,如表 3-2 所示。在该例 中每个工作记录包含 5 个数据项:第一项为返回的地址,为了叙述方便,在此以语句行号代替, 后四项为函数的 4 个参量。 表 3-2 Hanoi(3,x,y,z)函数的递归调用过程 层次. 执行编号. 栈状态. 实参. 塔与圆盘状态 X. 1. 2. 1-2-4-5. 1-2-4-5. 0,3,x,y,z. 6,2,x,z,y. (3,x,y,z). Y. 说明 Z. 1 2 3. 进入下层前,需将 返回地址和本层参 量进栈. 同上. (2,x,z,y). 第一次调用 Hanoi (3,x,y,z)后进入语句 5 之前的状态. 0,3,x,y,z. X. Y. Z. 6,1,x,y,z 3. 1-2-3-8-9. 6,2,x,z,y. (1,x,y,z). 0,3,x,y,z. 2 3. X. 2. 6-7. 6,2,x,z,y. 1. Y. 3. X. 2. Y. 8,1,z,x,y 3. 1-2-3-8-9. 6,2,x,z,y 0,3,x,y,z. Z. (2,x,z,y). 0,3,x,y,z. (1,z,x,y) 3. 执行语句 3 时将 1 号盘从 x 移到 z;从 语句 9 退出本层返 回上层. 1 2. 1. Z. 执行语句 6 时将 2 号盘从 x 移到 y;准 备执行语句 7 进入 下层的递归调用 执行语句 3 时将 1 号盘从 z 移到 y;从 语句 9 退出本层返 回上层.

(20) 数据结构(C 语言版). 64. 续表 层次. 执行编号. 栈状态. 实参. 塔与圆盘状态. 说明 从语句 9 退出本层, 继续退栈. 2. 8-9. 6,2,x,z,y. 同上. (2,x,z,y). 0,3,x,y,z. X. 1. 2. 6-7. 1-2-4-5. 0,3,x,y,z. 8,2,y,x,z. Y. (3,x,y,z). 执行语句 6 时将 3 号盘从 x 移到 z;准 备执行语句 7 进入 下层的递归调用. Z. 1 2. 3. 进入下层前,需将 返回地址和本层参 量进栈. 同上. (2, y,x,z). 0,3,x,y,z. X. Y. Z. 6,1,y,z,x 3. 1-2-3-8-9. 8,2,y,x,z. (1, y,z,x) 1. 2. 执行语句 3 时将 1 号盘从 y 移到 x;从 语句 9 退出本层返 回上层. 3. 0,3,x,y,z X. 2. 6-7. 8,2,y,x,z. Y. (2, y,x,z). 2 3. 1. 0,3,x,y,z. X. Y. 8,1,x,y,z 3. 1-2-3-8-9. 8,2,y,x,z. 8-9. 8,2,y,x,z. (2,y,x,z). Z 1 2 3. (1, x,y,z). 0,3,x,y,z. 2. Z. 同上. 执行语句 6 时将 2 号盘从 y 移到 z;准 备执行语句 7 进入 下层的递归调用 执行语句 3 时将 1 号 盘从 x 移到 z;退栈, 从语句 9 退出第 3 层,返回第 2 层. 继续退栈,从语句 9 退出第 2 层,返回 第1层. 0,3,x,y,z. 1. 8-9. 0,3,x,y,z. (3,x,y,z). 同上. 退栈,从语句 9 退 出第 1 层,返回主 函数 继续运行主函数. 0. 同上.

(21) 第 3 章 栈和队列. 3.4. 65. 队列. 3.4.1 队列的定义和运算 1.队列的定义 队列也是一种运算受限的线性表。在这种线性表上,插入限定在表的某一端进行,删除 限定在表的另一端进行。允许插入的一端称为队尾,允许删除的一端称为队头。新插入的元素 只能添加到队尾,被删除的只能是排在队头的元素。 图 3-10 是一个队列的示意图。队列中已顺序存放了 A,B,C,D,E,F6 个数据元素。其中,A 处于队头,F 处于队尾。如果要插入一个新的数据元素 G,只能在队尾 F 之后进行。如果要删 除一个数据元素,只能在当前队头进行。本图中只能删除当前队头元素 A。插入的过程称为入 队,删除的过程称为出队。队头和队尾的位置在插入和删除过程中是不断变化的。由于最先入 队的数据元素 A 放在队头,总是最先出队。而最后入队的数据元素 F 放在队尾,要想出队, 只有等它前面的元素全部出队以后,F 才能出队。队列中数据元素的入队和出队过程是按照“先 进先出”(First In First Out)的原则进行的。因此,队列又称为“先进先出”的线性表,简称 FIFO 表。 出队列. 入队列 A. B. C. D. E. F. 队尾. 队头. 图 3-10. 队列示意图. 队列与现实生活中人们的购物、购票等排队过程十分相似。排队的规则是不允许“插队”, 新加入的成员只能排在队尾,而且队中全体成员只能按顺序向前移动,当到达队头(得到服务) 后离队。 2.队列的基本运算 队列(简称队)可定义如下 5 种基本运算: (1)队列初始化 InitQueue(SQ):其作用是设置一个空队列 SQ。 (2)入队列 EnQueue(SQ,x):其作用是将 x 插入到队列 SQ 的队尾。 (3)出队 OutQueue(SQ,x):将队头元素赋给 x,并删除队头元素。 (4)取队头元素 GetHead(SQ,x):由 x 返回队头结点的值。 (5)判队列空 Empty (SQ):队列 SQ 是否为空。若为空返回 1,否则返回 0。 3.4.2 队列的顺序存储结构及其基本运算的实现 与栈类似,队列通常有两种存储结构,即顺序存储和链式存储。 1.队列的顺序存储结构 队列的顺序存储结构简称顺序队。顺序队是用一维数组依次存放队列中的元素和分别指 示队列的首端和队列的尾端的两个变量组成。这两个变量分别称为“队头指针”和“队尾指针”。.

(22) 数据结构(C 语言版). 66. 顺序队列的数据类型定义如下: #define MaxSize < 队列的最大容量> //根据具体情况确定 typedef struct{ ElemType data[MaxSize]; int front,rear; }SQUEUE;. 顺序队定义为一个结构类型。该类型变量有 3 个域:data、front、rear。其中 data 为存储 队列中元素的一维数组。数组的低下标一端为队头,高下标一端为队尾。队头指针 front 和队 尾指针 rear 定义为整型变量。 为方便起见,约定头指针 front 总是指向队头的前一位置(即指示队头元素在一维数组中 的当前位置的前一个位置)。尾指针 rear 指向队尾元素的所在位置(即指示队尾元素在一维数 组中的当前位置)。队列的存储空间为从 data[1]到 data[MaxSize-1]。此时队列为空的条件为 front=rear=0。虽然浪费一个存储单元 data[0],但可以给我们的操作带来方便。图 3-11 展示了 MaxSize=5 时顺序队列的几种状态。 4. 4. 3. rear 3. C. 2 1. 2 rear 1. A. rear 4 front 3. 3. 2. 2. 1. 1. 1. 0. 0. 0. rear. 4 rear. 4. B. 3 front 2. 3 front 2. A. 1 0. D. 4. rear 0 front. front. (a). 0 front. (b). (c). (d). (e). (f). 图 3-11 顺序队列中进出队列指针变化情况. (a)表示空队列,rear=front=0。 (b)元素 A 入队后,rear=1,front=0。 (c)B,C 依次入队后,rear=3,front=0。 (d)A,B,C 依此出队后,rear=front=3。 (e)D 入队后,rear=4,front=3。 (f)D 出队后,rear=front=4。 从图 3-11 中可以看到,在队列刚建立时为空队列,令 front=rear=0。每当加入一个新元素 时,让队尾指针 rear 增 l,再将新元素添加到 rear 所指位置。因而指针 rear 指示了实际的队尾 位置。而队头指针 front 则不然,它实际指示的是真正队头元素所在位置的前一位置。所以, 如果要退出队头元素时,应当首先让队头指针 front 增 l,再返回 front 所指位置上的元素值。 如果队头指针 front=rear,队列为空。若一维数组中所有位置上都被元素装满,即尾指 针 rear 指向一维数组最后,而头指针指向一维数组开头,称为队满。但有可能出现这样的 情况,尾指针指向一维数组最后,但前面有很多元素已经出队,即空出很多位置。这时要 插入元素,仍然会发生溢出。例如,在图 3-11(e)中,rear=MaxSize-1=4,再进队时将发 生溢出。但是,这种“溢出”并不是真正的溢出,在 data 数组的前端可能还有空位置,所 以这是一种“假溢出”。.

(23) 第 3 章 栈和队列. 67. 克服“假溢出”的方法有两种。一种是将队列中的所有元素均向最前端的位置移动,显 然这种方法是很浪费时间的,所以在实际应用中很少采用。另一种方法是采用循环队列。即将 所用的数组 data[0] 到 data[MaxSize-1]想象成首尾相连接的循环数组,设想 data[0] 接在 data[MaxSize-1]之后,将这种形式的顺序队列称为循环队列。如图 3-12 所示是具有 5 个存储 单元的循环队列。 1. 2. rear. 1. 2. 1. 2. A 0 3 4. 3 rear front. (a). A. B 0. (b). rear. 4. 0 3. D front. rear. (c) 图 3-12. 0. C 3. D front. 4. 1. 2. B 0. C 3. 1 front. 2. 4. (d). front. rear. 4. (e). 循环队列的几种状态. (a)表示空队列,rear= front=0。 (b)元素 A 入队后,rear=1,front=0。 (c)B,C,D 依次入队后,rear=4,front=0。 (d)A 出队后, front=1 ,rear=4。 (e)B,C,D 出队后, rear= front=4。 初始循环队列空的标志为 front=rear=0。循环队列首尾相连,当队头指针 front=MaxSize-1 后,再前进一个位置就自动到 0,这可以利用除法取余的运算(%)来实现。所以循环队列出 队 列 时 头 指 针 变 化 为 front=(front+1)%MaxSize ; 循 环 队 列 入 队 时 , 队 尾 指 针 变 化 为 rear=(rear+1)%MaxSize。从图 3-12 中可以看出,循环队列空的标志为 rear=front,循环队列满 的标志为(rear+1)% MaxSize=front,此时队列中实际尚留有一个空间。这是为了避免与队空标 志相冲突,以一个元素所占用的存储空间为代价,以节省执行时间。所以,具有 MaxSize 个 元素的循环队列最多只能存放 MaxSize-1 个数据元素。当然也可另设一个标志位以区别队列是 “空”还是“满”,但运算时就要多花费一些时间。 2.基本运算顺序队列的实现 循环队列的 5 种基本运算函数如下: (1)队列初始化。 void InitQueue(SQUEUE *SQ) { SQ->rear=SQ->front=0;}. (2)入队列。 int EnQueue(SQUEUE *SQ,ElemType x) { if((SQ->rear+1)%MaxSize==SQ->front){ printf("\n Queue is full!"); return 0; } SQ->rear=(SQ->rear+1)%MaxSize;.

(24) 数据结构(C 语言版). 68 SQ->data[SQ->rear]=x; return 1; }. (3)判队列空。 int Empty(SQUEUE *SQ) { return(SQ->rear==SQ->front)?1:0;}. (4)出队。 int OutQueue(SQUEUE *SQ,ElemType *x) { if(Empty(SQ)){ printf("\n Queue is free"); return 0; } SQ->front=(SQ->front+1)%MaxSize; *x=SQ->data[SQ->front]; return 1; }. (5)取队头元素。 int GetHead(SQUEUE *SQ,ElemType *x) { if(Empty(SQ)){ printf("\n Queue is free"); return 0; } *x=SQ->data[(SQ->front+1)%MaxSize]; return 1; }. 3.4.3 队列的链式存储结构及其基本运算的实现 1.队列的链式存储结构 队列的链式存储结构简称为链队,它实际上是一个同时带有首指针和尾指针的单链表。 为了操作方便起见,给链队列添加一个表头结点,并令头指针指向表头结点,而尾指针则指向 队尾元素。如图 3-13 所示。 data. next. .... front. ^ rear. 图 3-13. 链队示意图. 链队的数据类型定义如下: typedef struct qnode{ ElemType data; struct qnode *next; }QTYPE; typedef struct qptr{. //链队结点的类型. //链队指针类型.

(25) 第 3 章 栈和队列. 69. QTYPE *front,*rear; }SQUEUE; SQUEUE LQ;. 图 3-14 展示了链队运算指针变化情况。 front. ^. LQ rear (a)队空. front A. LQ. ^. rear (b)元素 A 入队后 front A. LQ. B. ^. B. ^. rear (c)元素 B 入队后. front A. LQ rear. (d)元素 A 出队. 图 3-14. 链队运算指针变化示意图. 在这样的链队中,队空的条件是头指针和尾指针均指向表头结点,即 LQ.front==LQ. rear。 和链栈情况相同,除非整个存储空间全部用完,一般情况下,链队不会出现队满的情况。 2.基本运算链队的实现 下面给出在链队上实现队列基本运算的算法。 (1)队列初始化。 void InitQueue(SQUEUE *LQ) { QTYPE *p; p=(QTYPE *)malloc(sizeof(QTYPE)); p->next=NULL; LQ->front= LQ->rear=p; }.

(26) 数据结构(C 语言版). 70. (2)入队列。 int EnQueue(SQUEUE *LQ,ElemType x) { QTYPE *s; s=(QTYPE *)malloc(sizeof(QTYPE)); //创建新结点,插入到链队的末尾 s->data=x; s->next=LQ->rear->next; LQ->rear->next=s; LQ->rear=s; return 1; }. (3)判队空。 int Empty(SQUEUE *LQ) { return(LQ->front==LQ->rear?1:0);}. (4)出队列。 int OutQueue(SQUEUE *LQ,ElemType *x) { QTYPE *p; if(Empty(LQ)){ printf("\n Queue is free!"); return 0; } p=LQ->front->next; *x=p->data; LQ->front->next=p->next; if(LQ->front->next==NULL) //若原队列中只有一个结点,删除后队列变空 LQ->rear=LQ->front; free(p); return 1; }. (5)取队头元素。 int GetHead(SQUEUE *LQ,ElemType *x) { if(Empty(LQ)){ printf("\n Queue is free!"); return 0; } *x=LQ->front->next->data; return 1; }. 3.4.4 队列应用举例 利用队列“先进先出”的特点,在日常生活中和计算机程序设计中可以解决许多实际问题。 例如操作系统中的作业排队,在允许多道程序运行的计算机系统中,同时有几个作业运 行。如果它们分别对同一个设备请求服务,而运行的结果都需要通过通道输出,则这些请求就 要按照在时间上的先后顺序进行排队,组成一个队列。每当通道传输完毕并可以接受新的输出 任务时,队头的作业先从队列中退出做输出操作。凡是申请输出的作业都从队尾进入队列。按 照先到先服务的原则,逐个完成多个作业的请求服务。.

(27) 第 3 章 栈和队列. 71. 在 Windows 中,应用程序都是基于事件和消息的,即应用程序的执行顺序取决于事件发 生的顺序,是由顺序产生的消息驱动的。Windows 为每一个应用程序维护相应的消息队列, Windows 每时每刻监视用户的每一操作,并分析用户的动作与哪一个应用程序有关,将用户的 动作以消息的形式发送给该应用程序(即放到应用程序的消息队列中)。应用程序不停地从它 的消息队列中获取消息、分析消息、处理消息,直到接收到一条 WM_QUIT 的消息为止,从 而使得程序的执行顺序由事件的发生顺序来控制。 【例 3-4】下面应用队列来解决舞伴问题。假设在周末舞会上,男士和女士各自排成一队。 跳舞开始时,依次从男队和女队的队头上各出一人配成舞伴。若两队初始人数不相同,则较长 的那一队中未配对者等待下一轮舞曲。现要求写一算法模拟上述舞伴配对问题。 【解】先入队的男士和女士先出队配成舞伴。因此该问题具有典型的先进先出特性,可用 队列作为算法的数据结构。假设参加跳舞的人员名单存放在数据文件“dancer.dat”中。每一条 记录存放一个人员的信息,包括姓名和性别。性别以字母 M(男性)和 F(女性)表示。如: M 陈建 M 王刚 F 李烨 … 首先创建两个队列,一个用来存放男士名单,另一个用来存放女士名单。然后打开文件, 读出有关信息。每读进一条记录,先判断是男士还是女士,再存入相应的队列中。最后分别对 两个队列执行出队运算。每当有一对男女出队,便组成一对舞伴,打印出他们的姓名,直到其 中有一个队列变空为止。若两队都为空,则表明男女人数相等,否则,将非空的一队队列中的 人数和排在队头的等待者的名字打印出来,他(或她)将是下一轮舞曲开始时第一个获得舞伴 的人。 #define MaxSize 100 typedef struct{ char name[20]; char sex; }PERSON; typedef struct{ PERSON data[MaxSize]; int front, rear; int count; //计数器,记录队中元素总数 }SQUEUE; void InitQueue(SQUEUE *sq) //队列初始化 { sq->rear=sq->front=0; sq->count=0; } int EnQueue(SQUEUE *sq,PERSON x) //入队列 { if((sq->rear+1)%MaxSize==sq->front){ printf("\n Queue is full!"); return 0;.

(28) 72. 数据结构(C 语言版) } sq->count++; sq->rear=(sq->rear+1)%MaxSize; sq->data[sq->rear]=x; return 1; } int Empty(SQUEUE *sq) //判队列空 { return(sq->rear==sq->front)?1:0; } int OutQueue(SQUEUE *sq,PERSON *x) //出队 { if(Empty(sq)){ printf("\n Queue is free"); return 0; } sq->front=(sq->front+1)%MaxSize; *x=sq->data[sq->front]; sq->count--; return 1; } int GetHead(SQUEUE *sq,PERSON *x) //取队头元素 { if(Empty(sq)){ printf("\n Queue is free"); return 0; } *x=sq->data[(sq->front+1)%MaxSize]; return 1; } void DancePartner() { FILE *fp; PERSON p; SQUEUE mdancer, fdancer; InitQueue(&mdancer); InitQueue(&fdancer); if((fp=fopen("dancer.dat","r"))==NULL){ printf("can\’t open the file!"); exit(0); } while(!feof(fp){ //读入跳舞者的姓名 p.sex=fgetc(fp); fgetc(fp); //清除空格 fgets(fp,10,p.name); if(p.sex=='F') //插入到合适的队列中 EnQueue(&fdancer,p); //进女队 else EnQueue(&mdancer,p); }.

(29) 第 3 章 栈和队列. 73. printf("舞伴是:\n"); while(!Empty(&fdancer) && !Empty(&mdancer)){ OutQueue(&fdancer,&p); //依次输出男、女舞伴名 printf("%s",p.name); OutQueue(&mdancer,&p); printf("%s",p.name); } if(!Empty(&fdancer)){ //输出女队剩余人数及队头女士的名字 printf("\n 还有%d 个女士等下一轮。\n",fdancer.count); GetHead(&fdancer,&p); printf("%s will be the first to get a partner.\n",p.name); } else if(!Empty(&mdancer)){ printf("\n 还有%d 个男士等下一轮。\n",mdancer.count); GetHead(&mdancer,&p); printf("%s 将得到第一个舞伴\n",p.name); } }. 本章小结 栈和队列都是运算受限的线性表,是软件设计中最基础的数据结构。 因为栈只能在一端(栈顶)插入和删除元素,所以具有后进先出的特性。顺序栈是较方 便的栈的存储形式,但可能会有溢出。 链栈是栈的链式存储结构,其运算实现同样较为方便。 栈中常用的 5 种运算为:初始化栈、判栈空、取栈顶元素、进栈、退栈。 栈是实现程序调用的基本数据结构。表达式计算、递归调用是两个最典型的应用实例。 递归是重要的程序形式,也是软件设计的基本技术和方法。 队列是只能在一端(队尾)插入、另一端(队头)删除元素的线性表,具有先进先出的 特性。 队列中常用的 5 种运算为:初始化队列、判队空、取队头元素、进队、出队。 队列的顺序存储一般采用循环队列形式,可以克服“假溢出”的缺点。但最大的存储容 量应为循环队列容量减 1。 链队列是队列的链式存储形式,通常要设置一个头尾指针。在这一结构上实现队列的运 算较为简捷。 队列是软件设计中有关排队问题的基础。. 习. 题. 一、选择题 1.栈和队列都是( )。 A.顺序存储的线性结构. B.限制存取点的线性结构.

(30) 74. 数据结构(C 语言版). C.链接存储的线性结构 D.限制存取点的非线性结构 2.一个栈的入栈序列是 a,b,c,d,e,则栈的不可能的输出序列是( ) 。 A.edcba B.decba C.dceab D.abcde 3.若已知一个栈的入栈序列是 l,2,3,…,n,其输出序列为 p1,p2,p3,…,pn,若 pl=n,则 pi 为 ( )。 A.i B.n=i C.n-i+l D.不确定 4.循环队列 SQ 采用数组空间 SQ.data[0,n-1]存放其元素值,已知其头尾指针分别是 front 和 rear,则当前队列中的元素个数是( ) 。 A.(rear-front+n)%n B.rear-front+l C.rear-front-1 D.rear-front 5.中缀表达式 A-(B+C/D)*E 的后缀形式是( ) 。 A.AB-C+D/E* B.ABC+D/E* C.ABCD/E*+D.ABCD/+E*6.一个队列的入列序列是 1,2,3,4,则队列的输出序列是( )。 A.4,3,2,1 B.1,2,3,4 C.1,4,3,2 D.3,2,4,1 7.若在一个大小为 6 的数组上实现循环队列,且当前 rear 和 front 的值分别为 0 和 3,当 从队列中删除一个元素,再加入两个元素后,rear 和 front 的值分别为( )。 A.1 和 5 B.2 和 4 C.4 和 2 D.5 和 l 8.用不带头结点的单链表存储队列,其队头指针指向队头结点,队尾指针指向队尾结点, 则在进行出队运算时( )。 A.仅修改队头指针 B.仅修改队尾指针 C.对头、队尾指针都要修改 D.对头、对尾指针都可能要修改 9.若进栈序列为 a,b,c,则通过入出栈运算可能得到的 a,b,c 的不同排列个数为( ) 。 A.4 B.5 C.6 D.7 10.设数组 data[m]作为循环队列 SQ 的存储空间,front 为队头指针,rear 为队尾指针, 则执行出队运算后其头指针 front 值为( )。 A.front=front+1 B.front=(front+1)%(m-1) C.front=(front-1)%m D.front=(front+1)%m 11.在一个链队中,假定 front 和 rear 分别为队首指针和队尾指针,则删除一个结点的运 算应执行( )。 A.front=front->next: B.rear=rear->next; C.rear=front->next; D.front=rear->next; 12.向一个栈顶指针为 hs 的链栈中插入结点*s 时,应执行( ) 。 A.hs->next=s; B.s->next=hs; hs=s; C.s->next=hs->next;hs->next=s; D.s->next=hs;hs=hs->next; 13.在具有 n 个单元的顺序循环队列中,假定 front 和 rear 分别为队首指针和队尾指针, 顺序表的下标下界从 0 开始,则判断队满的条件是( )。.

(31) 第 3 章 栈和队列. 75. A.(1+rear)%n=front B.(1+rear)%(n-1)=front C.1+rear%n=front D.(1+front)%n=rear 14.若已知一个栈的入栈序列是 l,2,3,…,30,其输出序列是 pl,p2,p3,…,pn,若 pl=30,则 pl0 为( )。 A.11 B.20 C.30 D.21 二、填空题 1.设长度为 n 的链队列用单循环链表表示,若只设头指针,则入队和出队运算的时间复 杂度分别为________和________;若只设尾指针,则入队和出队运算的时间复杂度分别为 ________和________。 2.基本线性表,栈和队列都是________结构。可以在基本线性表的________插入和删除 元素;对于栈只能在________插入和删除元素;对于队列只能在________插入元素和________ 删除元素。 3.一个栈的输入序列是 235614,则栈的输出序列 54321 是________。(可能或不可能) 4.对于顺序循环队列 Q[M],下标从 0 到 M-1,头尾指针分别为 F 和 R 表示,出队时, 队首指针循环加 l 可表示 F=________。 5.因为顺序栈的空间是有限的。因此,在进行插入运算时,可能会发生________。 6.假设以 S 和 X 分别表示进栈和退栈运算,则对输入序列 a,b,c,d,e 进行一系列栈运算 SSXSXSSXXX 之后,得到的输出序列为________。 7.栈顶的位置是随着________运算而变化的。 8.有如下递归函数: int dunno(int m) { int value; if(m==O) value=3; else value=dunno(m-1)+5; return(value); }. 执行语句 printf ("%d\n",dunno(3));的结果是________。 9.设 a 是含有 N 个分量的整数数组,写出求 n 个整数之和的递归定义________,写出求 n 个整数之积的递归定义________。 10.设栈 S 和队列 Q 的初始状态为空,元素 a,b,c,d,e,f 依次通过栈 S,一个元素出栈后即 进入队列 Q。若这 6 个元素出队列的顺序是 b,d,c,f,e,a,则栈 S 的容量至少应该是________。 三、简答题 1.什么是队列的“假溢出”现象?一般有哪几种处理方法? 2.试证明:若借助栈由输入序列 1,2,3,…,n,得到输出序列 P1,P2,P3,…,Pn(它是输入序列 的一个排列),则输出序列中不可能出现这样的情况:若 i<j<k,存在输出序列 Pk Pi Pj。 3.顺序队列一般应该组织成为环状队列的形式,而且一般队列头或尾其中之一应该特殊 处理。例如,队列为 listarray[0…n-1],队列头指针为 front,队列尾指针为 rear,则 listarray[rear] 表示下一个可以插入队列的位置。请解释其原因。.

(32) 数据结构(C 语言版). 76. 4.试推导求解 n 阶 Hanoi 塔问题至少需执行的 move 运算的次数。 四、算法设计题 1.利用两个栈 Sl 和 S2 模拟一个队列时,要求实现该队列的进队、出队、判队空 3 种 运算。 2.假设如题图 3-1 所示,火车调度站的入口处有 n 节硬席或软席车厢(分别以 H 和 S 表 示)等待调度。试编写算法,输出对这 n 节车厢进行调度的运算(入栈或出栈运算)序列,以 使所有的软席车厢都被调整到硬席车厢之前。. 题图 3-1. 火车调度站. 3.求两个正整数 m 和 n 的最大公约数可以用如下 gcd (m,n)公式表示:. m. n=0. gcd(n,m%n). n>0. gcd(m,n)= (1)编写一个计算 gcd(m,n)的递归过程。 (2)将上述过程转化成非递归过程。 (3)画出计算 gcd (20,6)的过程及栈的状态变化,给出计算结果。 4.如果希望循环队列中的元素都能得到利用,则需设置一个标志域 tag,并以 tag 的值为 0 或 1 来区分尾指针和头指针值相同时的队列状态是“空”还是“满”。试编写与此结构相应的入 队列和出队列的算法,并从时间和空间角度讨论设标志和不设标志这两种方法的使用范围(如 当循环队列容量较小而队列中每个元素占的空间较多时,哪一种方法较好) 。 5.用单链表实现队列,如题图 3-2 所示,并令 front=rear=NULL 表示队列为空,编写实 现队列的如下 5 种运算的函数: SetNull:将队列置成空队列。 GetFirst:返回队列的第一个元素。 EnQueue:把元素插入队列的后端。 DeQueue:删除队列的第一个元素。 Empty:判定队列是否为空。 rear front. a1. a2. 题图 3-2 循环队列. …. an.

(33) 第 3 章 栈和队列. 77. 6.若使用循环链表来表示队列,p 是链表中的一个指针。试基于此结构给出队列的插入 (EnQueue)和删除(DeQueue)算法,并给出 p 为何值时队列空。 7.编写程序将一个循环队列的内容倒置,该循环队列存储在一个数组 A[1..N-1]中,例如, 题图 3-3(a)中为倒置前的队列,题图 3-3(b)中为倒置后的队列。 rear 6. 6. 5. 7 front. 4. rear. f. 8 9. b 10. b. 2. 4 c d. 3. d c. 7 8. e a. 5 a. 3. e. 9. f 10. 2 1. 1 front (a). (b). 题图 3-3 循环队列. 8.已知 Ackermann 函数的定义如下: Ack(m,n)=. n+1 Ack(m-1,1) Ack(m-1,Ack(m,n-1)). 写出计算 Ack(m,n)的非递归算法。. m=0 m≠0,n=0 m≠0,n≠0.

(34)

參考文獻

相關文件

甲型禽流感 H7N9 H7N9 H7N9 H7N9 H7N9 H7N9 H7N9 H7N9 - - 疾病的三角模式 疾病的三角模式 疾病的三角模式 疾病的三角模式 疾病的三角模式

上述定理, 即 Dini, Lipschitz, Dirichlet 判别法, 给出函数能展开成 Fourier 级数的充分条件... 下面罗列几个例子,

微积分的创立是数学发展中的里程碑, 它的发展 和广泛应用开启了向近代数学过渡的新时期, 为研究 变量和函数提供了重要的方法和手段. 运动物体的瞬

下面我们将用 这一点将一元函数的 Taylor 展开推广到多元函数.. 我们希望借助此给出多元函数

为此, 我们需要建立函 数的差商与函数的导数间的基本关系式, 这些关系式称为“微分学中值定理”...

[初等函数] 幂函数、指数函数、对数函数、三角函数、反三角函数通称为“ 基本初等函

本章我们又一次经历了用函数研究变化规律的过程 ,用反比例函数刻画具 有反比例关系的两个变量之间的对应关系 :在变量y 随变量x 的变化而变化的

• 如果将坐标建立在以速率c运动的界面上,则c=0,Dh描述了生 长界面的扩散驰豫, 是非线性强度,与c成比例,h是表面梯度