510 likes | 705 Views
数 据 结 构. 第三章 栈和队列. 本章学习导读 栈和队列也是线性表 ------- 操作受限的线性表 : 栈 : 只允许在表的一端进行插入或删除操作 ; 队列 : 只允许在表的一端进行插入操作、而在 另一端 进行删除操作。 掌握栈和队列的 逻辑结构 和 存储结构 , 栈和队列的 基本运算 以及 实现算法 。. 第三章 栈和队列. 3.1 栈 3.1.1 抽象数据类型栈的定义 3.1.2 栈的表示和实现 3.2 栈的应用举例 3.2.1 数制转换 3.2.2 括号匹配的检验
E N D
数 据 结 构 第三章 栈和队列
本章学习导读 • 栈和队列也是线性表 -------操作受限的线性表: • 栈 :只允许在表的一端进行插入或删除操作; • 队列:只允许在表的一端进行插入操作、而在 • 另一端 进行删除操作。 • 掌握栈和队列的逻辑结构和存储结构, • 栈和队列的基本运算以及实现算法。
第三章 栈和队列 3.1 栈 3.1.1 抽象数据类型栈的定义 3.1.2 栈的表示和实现 3.2 栈的应用举例 3.2.1 数制转换 3.2.2 括号匹配的检验 3.2.4 行编辑程序 3.2.5 迷宫求解 3.2.5 表达式求值
3.1 栈 递归调用 求n!的递归方法的思路是: 相应的C语言函数是: float fact(int n) { float s; if (n= =0||n= =1) s=1; else s=n*fact(n-1); return (s); } 在该函数中可以理解为求n!用fact(n)来表示,则求(n-1)!就用fact(n-1)来表示。 若求5!,则有 void main( ) { printf(“5!=%f\n” , fact(5)) ; }
先 后 图3-1 递归调用过程示意图 主函数 mani() printf(“fact(5)”) 第一层调用 n=5 s=5*fact(4) 第二层调用 n=4 s=4*fact(3) 第三层调用 n=3 S=3*fact(2) 第四层调用 n=2 S=2*fact(1) 第五层调用 n=1 S=1 fact(1)=1 fact(2)=2 fact(3)=6 fact(4)=24 fact(5)=120 输出s=120.00
5 5 4 5 4 3 5 4 3 2 5 4 3 2 1 • 计算机系统处理上述过程时,其关键是要正确处理执行过程中的递归调用层次和返回路径,也就是要记住每一次递归调用时的返回地址。在系统中是用一个线性表动态记忆调用过程中的路径,其处理原则为: • 执行程序前,建立一个线性表,其初始状态为空。 • 递归调用时,将当前调用的返回点地址插入到线性表的末尾; • 递归调用返回时,其返回地址从线性表的末尾取出。 • 根据以上的原则,可以给出线性表中的元素变化状态如图3-2所示: • (以递归调用时n值的变化为例): 图3-2 递归调用时线性表状态
3.1.1 抽象数据类型栈的定义 • 1.栈的定义 • 栈(stack)是一种只允许在一端进行插入和删除的线性表,它是一种操作受限的线性表。 • 在表中只允许进行插入和删除的一端称为栈顶(top) • 另一端称为栈底(bottom) • 栈的插入操作通常称为入栈或进栈(push) • 栈的删除操作则称为出栈 或 退栈(pop) • 当栈中无数据元素时,称为空栈
出栈 入栈 栈顶 top an-1 ... a1 栈底 bottom a0 图3-3栈的示意图 据栈的定义可知,栈是按照后进先出 (LIFO,last in first out)的原则组织数据的,因此,栈也被称为“后进先出”的线性表。 • 指针 top 指示栈顶的位置 • 指针 bottom 指向栈底 • 指针 top 动态反映栈的当前位置
ADT Stack{ DO:D={ai | ai ElemSet, i=1,2,…n, n0} DR: R1={<ai-1,ai> | ai-1,ai D, i=2,…n} 约定an端为栈顶,a1端为栈底。 FO: } InitStack ( &S ) DestroyStack( &S ) ClearStack ( &S ); StackEmpty ( &S ) StackLength ( &S ); GetTop ( &S , &e ) Push( &S , &e ); Pop( &S , &e ) StackTraverser( S , visist() )
2.栈的基本运算 (1)initStack(s)初始化:初始化一个新的栈。 (2)empty(s)栈的非空判断:若栈s不空,则返回TRUE;否则, 返回FALSE。 (3)push(s,x)入栈:在栈s的顶部插入元素x,若栈满,则返回 FALSE;否则,返回TRUE。 (4)pop(s)出栈:若栈s不空,则返回栈顶元素,并从栈顶中删 除该元素;否则,返回空元素NULL。 (5)getTop(s)取栈元素:若栈s不空,则返回栈顶元素;否则返 回空元素NULL。 (6)setEmpty(s)置栈空操作:置栈s为空栈。 栈是一种特殊的线性表,因此栈可采用顺序存储结构存储, 也可以使用链式存储结构存储。
3.1.2 栈的顺序存储结构 • 1.顺序栈的数组表示 • 利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,这种形式的栈也称为顺序栈。 • 设指针top指向栈顶元素的当前位置 • 以数组小下标的一端作为栈底 • 通常top=0时为空栈,在元素进栈时指针top不断地加1 • 当top等于数组的最大下标值时则栈满
top=0时空栈 同时表示元素个数。 图3-4 顺序栈中数据元素与栈顶指针的变化。
typedef struct { SElemType * base; SElemType * top; int stacksize; } SqStack; 用C语言定义的顺序存储结构的栈如下: # define STACK_INIT_SIZE/*最大元素数个数*/ # define STACKINCREMENT/*元素空间增量*/ typedef struct { Elemtype stack[MAXNUM]; int top; } sqstack; (A) (B)
top a n-1 Max-1 … 4 3 2 1 0 Max-1 … 4 3 2 1 0 top base a n-1 a1 a1 top base a0 a0 1 2 1 2 top=-1 用C语言的一维数组 作为栈时,应设栈顶 top= -1时为空栈。 使用C语言的动态空间 作为栈时,应设栈顶指针 top= base时为空栈。 (A) 1,2 (B)1,2
Stack 2.顺序栈的基本运算算法 (1)初始化栈【算法3.1 栈的初始化】 Status InitStack(SqStack &s) { /*创建一个空栈由指针S指出*/ s.base=(SElemType*) malloc(STACK_INIT_SIZE *sizeof(SElemType)); if (s.base= =NULL)exit(OVERFLOW); s.top= s.base; s.stacksize=STACK_INIT_SIZE; return OK; } s.top= s.base
(2)入栈操作【算法3.2 入栈操作】 Status Push(SqStack & s, SElemtype e) { /*将元素x插入到栈s中,作为s的新栈顶*/ if(s.top-s.base>=s.stacksize) { /*栈满*/ s.base=(SElemType*)realloc(s.base, (s.stacksize+STACKINCREMENT)*sizeof(SElemType)); if(!s.base) exit(OVERFLOW); s.top=s.base+s. stacksize; s.stacksize+= STACKINCREMENT; } *s.top++=e; return OK; } IncreM Stack *s.top=e; s.top++;
s.top s.base a i a1 a0 (3)出栈操作【算法3.3 出栈操作】 Status Pop(SqStack &s, SElemtype &e) { /*若栈s不为空,则删除栈顶元素*/ if(s.top==s.base) return ERROR; /*栈空*/ e=*--s.top; /*先移动指针后引用*/ return OK; } --s.top; e=s.top;
ai+1 a i s.top s.base a1 a0 (4)取栈顶元素操作【算法3.4 取栈顶元素】 Status GetTop(SqStack s, SElemtype &e) { /*若栈s不为空,则返回栈顶元素*/ if(s.top==s.base) return ERROR; /*栈空*/ e=*(s.top-1); /*top所指的位置没有元素*/ return OK; } 取栈顶元素与出栈不同之处在于出栈操作改变栈顶指针top的位置,而取栈顶元素操作不改变栈的栈顶指针。
top base (5)判栈空操作【算法3.5 判栈空操作】 Status Empty(sqstack *s) { /*栈s为空时,返回为TRUE;非空时,返回为FALSE*/ if(s top==s base) return TRUE; return FALSE; }
top base (6)置空操作【算法3.6 栈置空操作】 void SetEmpty(sqstack *s) { /*将栈s的栈顶指针top,置为base*/ s top= s base; }
3.1.3 多栈共享邻接空间 • 在计算机系统软件中,编译系统都使用栈。 • 一个程序中常用到多个栈 • 每个栈存储空间足够大 • 实际中很难准确估计 • 预分配过大的存储空间会造成系统空间紧张。 • 若让多个栈共用一个足够大的连续存储空间,则可利用栈的动态特性使它们的存储空间互补。这就是栈的共享邻接空间。
1.双向栈在一维数组中的实现 栈的共享中最常见的是两栈的共享。假设两个栈共享一维数组stack[MAXNUM],则可以利用栈的“栈底位置不变,栈顶位置动态变化”的特性,两个栈底分别为-1和MAXNUM,而它们的栈顶都往中间方向延伸。因此,只要整个数组stack[MAXNUM]未被占满,无论哪个栈的入栈都不会发生上溢。 C语言定义的这种两栈共享邻接空间的结构如下: typedef struct { Elemtype stack[MAXNUM]; int lefttop; /*左栈栈顶位置指示器*/ int righttop; /*右栈栈顶位置指示器*/ } dupsqstack; 两个栈共享邻接空间的示意图如图3-5所示。左栈入栈时,栈顶指针加1,右栈入栈时,栈顶指针减1。
为了识别左右栈,必须另外设定标志: char status; status=’L’; /*左栈*/ status=’R’; /*右栈*/ 在进行栈操作时,需指定栈号:status=’L’为左栈,status=’R’为右栈;判断栈满的条件为: s->lefttop+1= =s->rigthtop; 自由区 0 MAXNUM-1 lefttop rightto 图3-5 两个栈共享邻接空间
s->righttop =MUXNUM s->lefttop =-1 图3-5 两个栈共享邻接空间 2.共享栈的基本操作 (1)初始化操作【算法3.7 共享栈的初始化】 int initDupStack(dupsqstack *s) { /*创建两个共享邻接空间的空栈由指针S指出*/ if ((s=(dupsqstack*)malloc(sizeof(dupsqstack)))= =NULL) return FALSE; s->lefttop= -1; s->righttop=MAXNUM; return TRUE; }
(2)入栈操作【算法3.8 共享栈的入栈操作】 int pushDupStack(dupsqstack *s,char status,Elemtype x) { /*把数据元素x压入左栈(status=’L’)或右栈(status=’R’)*/ if(s->lefttop+1= =s->righttop) return FALSE; /*栈满*/ if(status=’L’) s->stack[++s->lefttop]=x; /*左栈进栈*/ else if(status=’R’) s->stack[--s->lefttop]=x; /*右栈进栈*/ else return FALSE; /*参数错误*/ return TRUE; }
(3)出栈操作【算法3.9 共享栈的出栈操作】 Elemtype popDupStack(dupsqstack *s,char status) { /*从左栈(status=’L’)或右栈(status=’R’)退出栈顶元素*/ if(status= =’L’) { if (s->lefttop<0) return NULL; /*左栈为空*/ else return (s->stack[s->lefttop--]); /*左栈出栈*/ } else if(status= =’R’) { if (s->righttop>MAXNUM-1) return NULL; /*右栈为空*/ else return (s->stack[s->righttop++]); /*右栈出栈*/ } else return NULL; /*参数错误*/
3.1.4 栈的链式存储结构 • 栈也可以采用链式存储结构表示,这种结构的栈简称为链栈。 • 在一个链栈中,栈底就是链表的最后一个结点,而栈顶总是链 表的第一个结点 。因此 ,新入栈的元素即为链表新的第一个结 点,只要系统还有存储空间,就不会有栈满的情况发生。 • 一个链栈可由栈顶指针top唯一确定,当top为NULL时,是一个空栈。图3-6给出了链栈中数据元素与栈顶指针top的关系。
链栈的C语言定义为: typedef struct Stacknode { Elemtype data; Struct Stacknode *next; }slStacktype;
(1)入栈操作【算法3.10 单个链栈的入栈操作】 int pushLstack(slStacktype *top, Elemtype x) { /*将元素x压入链栈top中*/ slStacktype *p; if((p=(slStacktype *)malloc(sizeof(slStacktype)))==NULL) return FALSE; /*申请一个结点*/ p->data=x; p->next=top; top=p; return TRUE; } (2)出栈操作【算法3.11 单个链栈的出栈操作】 Elemtype popLstack(slStacktype *top) { /*从链栈top中删除栈顶元素*/ slStacktype *p; Elemtype x; if (top= =NULL) return NULL; /*空栈*/ p=top; top=top->next; x=p->data; free(p); return x; }
2.多个链栈的操作 程序中同时使用两个以上的栈时,使用顺序栈共用邻接空间很不方便,但若用多个单链栈时,操作极为方便,这涉及多个链栈的操作。我们可将多个单链栈的栈顶指针放在一个一维数组 slStacktype *top[M];中,让top[0],top[1],…,top[i],…,top[M-1]指向M个不同的链栈。 top[0] top[1] , … , top[i] … top[M-1]
(1)入栈操作【算法3.12 多个链栈的入栈操作】 int pushDupLs(slStacktype *top[M], int i, Elemtype x) { /*将元素x压入链栈top[i]中*/ slStacktype *p; if((p=(slStacktype *)malloc(sizeof(slStacktype)))= =NULL) return FALSE; /*申请一个结点*/ p->data=x; p->next=top[i]; top[i]=p; return TRUE; }
(2)出栈操作【算法3.13 多个链栈的出栈操作】 Elemtype popDupLs(slStacktype *top[M], int i) {/*从链栈top[i]中删除栈顶元素*/ slStacktype *p; Elemtype x; if (top[i]= =NULL) return NULL; /*空栈*/ p=top[i]; top[i]=top[i]->next; x=p->data;free(p); return x; } 在上面的两个算法中,当指定栈号i(0≤i≤M-1)时,则只对第i个链栈 操作,不会影响其它链栈。
3.2.1算术表达式求值 表达式求值是程序设计语言编译中的一个最基本问题。它的实现方法是栈的一个典型的应用实例。在计算机中,任何一个表达式都是由 操作数(operand) 运算符(operator) 界限符(delimiter)组成。 其中操作数可以是常数,也可以是变量或常量的标识符;运算符可以是算术运算体符、关系运算符和逻辑符;界限符为左右括号和标识表达式结束的结束符。
在本节中,仅讨论简单算术表达式的求值问题。在这种表达式中只含加、减、乘、除四则运算,所有的运算对象均为单变量。表达式的结束符为“#”。在本节中,仅讨论简单算术表达式的求值问题。在这种表达式中只含加、减、乘、除四则运算,所有的运算对象均为单变量。表达式的结束符为“#”。 算术四则运算的规则为: (1)先乘除、后加减; (2)同级运算时先左后右; (3)先括号内,后括号外。 计算机系统在处理表达式前,首先设置两个栈: (1)操作数栈(OPRD):存放处理表达式过程中的操作数。 (2)运算符栈(OPTR):存放处理表达式过程中的运算符。开始时,在运算符栈中先在栈底压入一个表达式的结束符“#”。 表3-1给出了+、-、*、/、(、)、和#的算术运算符间的优先级的关系。
计算机系统在处理表达式时,从左到右依次读出表达式中的各个符号(操作数或运算符),每读出一个符号后,根据运算规则作如下的处理:计算机系统在处理表达式时,从左到右依次读出表达式中的各个符号(操作数或运算符),每读出一个符号后,根据运算规则作如下的处理: (1)假如是操作数,则将其压入操作数栈,并依次读下一个符号。 (2)假如是运算符,则: 1)假如读出的运算符的优先级大于运算符栈栈顶运算符的优先 级 ,则将其压入运算符栈,并依次读下一个符号。 2)假如读出的是表达式结束符“#”,且运算符栈栈顶的运算符 也为“#”,则表达式处理结束,最后的表达式的计算结果在 操作数栈的栈顶位置。 3)假如读出的是“(”,则将其压入运算符栈。 4) 5)
4)假如读出的是“)”,则: A)若运算符栈栈顶不是“(”,则从操作数栈连续退出两个操作数, 从运算符栈中退出一个运算符,然后作相应的运算,并将运算 结 果压入操作数栈,然后继续执行A)。 B)若运算符栈栈顶为“(”,则从运算符栈退出“(”,依次读下一个 符号。 5)假如读出的运算符的优先级 <= 运算符栈栈顶运算符的优先级,则从操作数栈连续退出两个操作数, 运算符栈中退出一个运算符 作相应的运算 运算结果入操作数栈。此时读出的运算符下次重新考虑(即不读入下一个符号)。
top 2 - ( top / 5 + OPRD 4 # 6 # top OPTR top OPRD OPTR (b)读出5,+,(,6,-,4,/,2 (a)初始状态 top top - 6 top 2 ( ( top + 4 + 5 5 # # OPRD OPTR (c)读出),作运算T1=4/2=2 (d)作运算T2=6-2=4 OPRD OPTR top top 3 * + 4 top top 4 + # 5 5 # OPTR OPRD OPRD OPTR (f)读出*,3 (e)退( top top + 18 top top 23 # # 5 OPRD OPTR OPRD OPTR (h)重新考虑#,作运算4=5+18=23 (g)读#,作运算T3=6*3=18 图3-7 表达式的计算过程 图3-7给出了表达式5+(6-4/2)*3的计算过程,最后的结果为T4,置于OPRD的栈顶。
1、中缀表达式:如 A+B(除单目运算符外)。中缀表达式有时必须借助括号才能将运算顺序表达清楚,处理起来比较复杂。 2、逆波兰式:在编译系统中,对表达式的处理采用的是另外一种方法,即将中缀表达式转变为后缀表达式,然后对后缀式表达式进行处理,后缀表达式也称为逆波兰式。 波兰表示法(也称为前缀表达式)是由波兰逻辑学家(Lukasiewicz)提出的,如A+B表示为 +AB; 3、逆波兰式则:如A+B表示为AB+。中缀表达式经过上述处理后,运算时按从左到右的顺序进行,不需要括号。 得到后缀表达式后,我们在计算表达式时,可以设置一个栈,从左到右扫描后缀表达式,每读到一个操作数就将其压入栈中;每到一个运算符时,则从栈顶取出两个操作数进行运算,并将结果压入栈中,一直到后缀表达式读完。最后栈顶就是计算结果。
3.2.2 数制转换 十进制N和其它进制数的转换是计算机实现计算的基本问题,其解决方法很多,其中一个简单算法基于下列原理: N=(n div d)*d+n mod d ( 其中:div为整除运算,mod为求余运算) 例如 (1348)10=(2504)8,其运算过程如下:
n n div 8 n mod 8 1348 168 4 168 21 0 21 2 5 2 0 2
void conversion( ) { int e; initstack(s); scanf (“%”,n); while(n){ push(s,n%8); n=n/8; } while(! Stackempty(s)){ pop(s,e); printf(“%d”,e); } }
括号匹配 3.2.3 括号匹配的检验 假设表达式中允许括号嵌套,则检验括号是否匹配的方法可用“期待的急迫程度”这个概念来描述。例: (()() (())) 3.2.4 行编辑程序 在编辑程序中,设立一个输入缓冲区,用于接受用户输入的一行字符,然后逐行存入用户数据区。允许用户输入错误,并在发现有误时可以及时更正。
行编辑程序 行编辑程序算法如下: void lineedit( ){ initstack(s); ch=gether( ); while(ch!=eof){ while(ch!=eof && ch!=‘\n’){ switch(ch){ case ‘#’ : pop(s,c); case ‘@’ : clearstack(s); default : push(s,ch); } ch=getchar( ); } clearstack(s); if(ch!=eof) ch=gethar( ); } destroystack(s); }
3.2.5 迷宫求解 入口 出口
本章小结 本章主要介绍了如下一些基本概念: 栈:是一种只允许在一端进行插入和删除的线性表,它是一种操作受限的线性表。在表中只允许进行插入和删除的一端称为栈顶(top),另一端称为栈底(bottom)。栈顶元素总是最后入栈的,因而是最先出栈;栈底元素总是最先入栈的,因而也是最后出栈。因此,栈也被称为“后进先出”的线性表。 栈的顺序存储结构:利用一组地址连续的存储单元依次存放自栈底到栈顶的各个数据元素,称为栈的顺序存储结构。
双向栈:使两个栈共享一维数组stack[MAXNUM],利用栈的“栈底位置不变,栈顶位置动态变化”的特性,将两个栈底分别设为-1和MAXNUM,而它们的栈顶都往中间方向延伸的栈称为双向栈。双向栈:使两个栈共享一维数组stack[MAXNUM],利用栈的“栈底位置不变,栈顶位置动态变化”的特性,将两个栈底分别设为-1和MAXNUM,而它们的栈顶都往中间方向延伸的栈称为双向栈。 栈的链式存储结构:栈的链式存储结构就是用一组任意的存储单元(可以是不连续的)存储栈中的数据元素,这种结构的栈简称为链栈。在一个链栈中,栈底就是链表的最后一个结点,而栈顶总是链表的第一个结点。
除上述基本概念以外,学生还应该了解: • 栈的基本操作(初始化、栈的非空判断、入栈、 • 出栈、取栈元素、置栈空操作) • 栈的顺序存储结构的表示、栈的链式存储结构的 • 表示)。
栈和队列及其应用1 • 停车场管理 • 问题描述: 设停车场是一个可停放n辆汽车的狭长通道,且只有一个大门可供汽车进出. 汽车在停车场内按车辆到达先后顺序,依次由北向南(大门在最南端,最先到达的第一辆停放在车场最北端),如车场内已停满n辆汽车,则后来的汽车只能在门外的便道上等候,一旦有车开走,则排在便道上的第一辆车即可开入;当停车场内某辆车要离开时,在其后进入车辆必须先退出车场为它让路,待该辆车开出大门外,其他车辆再按原次序进入车场,每辆停放在它离开停车场时必须按它停留的时间长短交纳费用.编模拟程序. • 基本要求:
基本要求: 以栈模拟停车场,以队列模拟车场外的便道,按照从终端输入的数据序列进行模拟管理。每一组输入数据包括三个数据项: 汽车“到达”或“离去”信息、 汽车牌照号 以及“到达”或“离去的时刻. 对每一组输入数据进行操作后的输出信息为:若是车辆到达,则输出汽车在停车场或便道上的位置;若是车辆离去,则输出汽车在停车场停留的时间和应交纳的费用(在便道上停留的时间不收费).栈以顺序结构实现,队列以链表结构实现.
习题三 1.简述栈和线性表的区别和联系。 2.何为栈和队列?简述两者的区别和联系。 3.若依次读入数据元素序列{a,b,c,d}进栈,进栈过程中允许出栈,试写出各种可能的出栈元素序列。 4.将下列各算术运算式表示成波兰式和逆波兰式: (A*(B+C)+D)*E-F*G A*(B-D)+H/(D+E)-S/N*T (A-C)*(B+D)+(E-F)/(G+H) 5.写出算术运算式3+4/25*8-6的操作数栈和运算符栈的变化情况。 6.若堆栈采用链式存储结构,初始时为空,试画出a,b,c,d四个元素依次进栈后栈的状态,然后再画出此时的栈顶元素出栈后的状态。 7.试写出函数Fibonacci数列: F1=0 (n=1) F2=1, (n=2) ┆ Fn=Fn-1+Fn-2 (n>2) 的递归算法(和非递归算法)。