1、第三章 栈和队列,掌握栈和队列这两种抽象数据类型的特点,并能在相应的应用问题中正确选用它们。熟练掌握栈类型的两种实现方法,即两种存储结构表示时的基本操作实现算法,特别应注意栈满和栈空的条件以及它们的描述方法。 熟练掌握循环队列和链队列的基本操作实现算法,特别注意队满和队空的描述方法。,学习提要,课前思考,1. 什么是线性结构?2. 你见过餐馆中一叠一叠的盘子吗?如果它们是按1,2,n 的次序往上叠的,那么使用时候的次序应是什么样的? 3. 在日常生活中,为了维持正常的社会秩序而出现的常见现象是什么?,栈和队列是两种常用的数据类型,栈和队列是两种特殊的线性表,是操作受限的线性表,称限定性DS,栈
2、必须按“后进先出“的规则进行操作,而队列必须按“先进先出“的规则进行操作,只能在表的“端点”插入和删除进行的线性表,栈只允许在表尾一端进行插入和删除 ;队列只允许在表尾一端进行插入,在表头一端进行删除。,3.1 栈的类型定义,3.2 栈的应用举例,3.3 栈类型的实现,3.4 队列的类型定义,3.5 队列类型的实现,栈的定义和特点 定义:限定仅在表尾进行插入或删除操作的线性表,表尾对应栈顶,表头对应栈底,不含元素的空表称空栈。 往栈顶插入元素的操作为“入栈”,称删除栈顶元素的操作为“出栈”。 因为后入栈的元素先于先入栈的元素出栈,故被称为是一种“后进先出“的结构 特点:先进后出(FILO)或后
3、进先出(LIFO),栈(stack),铁路调度站形象地表示栈的这个特点,想一想:若要从栈中取出a1,该如何操作?,强调:插入和删除都只能在表的一端(栈顶)进行!,思考题:铁道进行火车调度时,总把站台设成栈式 (1)设有编号为1,2,3,4的四辆列车,顺序进入栈内,问可能得到哪些出栈顺序? 1,2,3,4 1,2,4,3 1,3,2,4 1,3,4,2 1,4,3,2 2,1,3,4 2,1,4,3 2,3,1,4 2,3,4,1 2,4,3,1 3,2,1,4 3,2,4,1 3,4,2,1 4,3,2,1 共14种 结论:当输入元素数目为n,即输入序列为1,2,n 时,经过栈的运算后可获得的
4、输出序列的个数由 尤.卡塔南数决定为:(2n)!/n!n!(n+1) (2)若进栈顺序为1,2,3,4,5,6问能否得到4,3,5,6, 1,2; 3,2,5,6,4,1; 1,5,4,6,2,3; 1,3,5,4,2,6的出栈顺序,为什麽?,讨论1:栈是什么?它与一般线性表有什么不同?,答:栈是一种特殊的线性表,它只能在表的一端(即栈顶)进行插入和删除运算。与一般线性表的区别:仅在于运算规则不同。,一般线性表 栈 逻辑结构:1:1 逻辑结构: 1:1 存储结构:顺序表、链表 存储结构:顺序栈、链栈 运算规则:随机存取 运算规则:后进先出(LIFO),“进”插入=压入=PUSH(an+1),“
5、出”删除=弹出=POP(an),ADT Stack 数据对象:Dai|aiElemSet, i=1,2,.,n, n0 数据关系:R| ai-1,aiD,i=2,.,n 约定an 端为栈顶,a1 端为栈底。 基本操作: InitStack(&S) 操作结果:构造一个空栈S。 DestroyStack(&S) 初始条件:栈S已存在。 操作结果:栈S被销毁。 ClearStack(&S) 初始条件:栈S已存在。 操作结果:将S清为空栈。 StackEmpty(S) 初始条件:栈S已存在。 操作结果:若栈S为空栈,则返回TRUE,否则FALE。,栈的抽象数据类型的定义:,基本操作: (续) Stac
6、kLength(S) 初始条件:栈S已存在。 操作结果:返回S的元素个数,即栈的长度。 GetTop(S, &e) 初始条件:栈S已存在且非空。操作结果:用e返回S的栈顶元素。 Push(&S, e) 初始条件:栈S已存在。 操作结果:插入元素e为新的栈顶元素。 Pop(&S, &e) 初始条件:栈S已存在且非空。操作结果:删除S的栈顶元素,并用e返回其值。 ADT Stack,栈的顺序存储结构简称为顺序栈,是利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,同时附设指针top指向实际栈顶后的空位置。,顺序栈,写入:Si= ai 读出: e= Si,压入(PUSH): Stop+=an
7、+1 弹出( POP) : e=S-top,an+1,以线性表 S= (a1 , a2 , . , an-1 , an )为例,前提:一定要预设栈顶指针top,顺序表和顺序栈的操作区别,顺序栈的类型定义如下:# define STACK_INIT_SIZE 100;/存储空间初始分配量#define LISTINCREMENT 10;/存储空间分配增量typedef structureSElemType *base; /栈底指针,在构造和销毁之前为nullSElemType *top; /栈顶指针int stacksize;/当前已分配的存储空间,以元素为单位SqStack;,Stacksiz
8、e 指示栈的当前可使用的最大容量。栈的初始化操作为:按设定的初始分配量进行第一次存储分配; base可称为栈底指针,在顺序栈中它始终指向栈底的位置,若base的值为NULL,则表明栈结构不存在。 top为栈顶指针,其初值指向栈底,即top=base可作为栈空的标记; 每当插入新的栈顶元素时(入栈),堆栈指针top先压后加(Stop+=an+1); ; 删除栈顶元素时(出栈),堆栈指针top先减后弹 (e=S-top) 非空栈中的栈顶指针始终在栈顶元素的下一个位置。,栈顶指针top,指向实际栈顶 后的空位置,初值为0,进栈,A,栈满,B,C,D,E,F,设数组长度为M: top=0,栈空,此时出
9、栈,则下溢(underflow) top=M,栈满,此时入栈,则上溢(overflow),栈空,栈满的条件 : top-base=stacksize;,在一个程序中同 时使用两个栈,1、构造一个空栈 int InitStack(SqStack ,顺序栈的基本操作,top,1,2,3,4,5,0,base,stacksize,2、返回栈顶元素 int GetTop(SqStack ,顺序栈的基本操作,顺序栈入栈,int push(SqStack ,A,B,C,D,插入E,E,顺序栈出栈算法: Status Pop(SqStack ,A,B,C,D,e=D,2、链栈 用链式存储结构实现的栈称为链栈
10、。通常链栈用单链表表 示,因此其结点结构与单链表的结构相同,在此用 LinkStack表示,即有:typedef struct node DataType data;struct node *next; StackNode,* LinkStack;/*说明top为栈顶指针*/LinkStack top ; 插入和删除操作仅限制在表头位置上进行,栈的链式存储结构,栈底,栈顶,由此可以看出:一个链栈由其栈顶指针唯一指定设top指向栈顶元素,则 top= NULL 表示栈空,链栈不必设头结点,因为栈顶(表头)操作频繁; 链栈一般不会出现栈满情况,除非没有空间导致malloc分配失败。 链栈的入栈、出
11、栈操作就是栈顶的插入与删除操作,修改指针即可完成。,几点说明:,LinkStack Push_LinkStack(LinkStack top, datatype x) StackNode *s;s=malloc(sizeof(StackNode);s-data=x; s-next=top;top=s;return top;,x,入栈算法,LinkStack Pop_LinkStack (LinkStack top, DataType *x) StackNode *p;if (top= =NULL)return NULL;else *x = top-data;p = top; top = top
12、-next; free (p); return top; ,a,a,出栈算法,3.2 栈的应用举例,例一、 数制转换,例二、 括号匹配的检验,例三、 行编辑程序问题,例五、 迷宫求解,例四、 表达式求值,例 :数制转换问题 将十进制数N转换为r进制的数,其转换方法利用辗转相除法,以N=3456,r=8为例转换方法如下:N N div 8 (整除) N % 8(求余)1348 168 4 低168 21 021 2 52 0 2 高 所以:(1348)10 =(2504)8 转换过程中每得到一位8进制数则进栈保存,转换完毕后依次出栈则正好是转换结果。,栈的应用,计算顺序,输出顺序,算法思想如下:
13、当N0时重复1,21、若 N0,则将N % r 压入栈s中 ,执行2;若N=0,将栈s的内容依次出栈,算法结束。2、用N / r 代替 N。,void conversion () InitStack(S); scanf (“%d“,N);while (N) Push(S, N % 8);N = N/8;while (!StackEmpty(S) Pop(S,e);printf ( “%d“, e ); / conversion,例二、 括号匹配的检验 假设在表达式中 ()或( )等为正确的格式,( )或( )或 ())均为不正确的格式。,例如考虑下列括号序列: ( ) 1 2 3 4 5 6
14、7 8检验括号是否匹配的方法可用“期待的急迫程度“这个概念来描述。即后出现的“左括弧“,它等待与其匹配的“右括弧“出现的“急迫“心情要比先出现的左括弧高。当计算机接受了第一个括号后,它期待着与其匹配的第八个括号的出现,然而等来的却是第二个括号,此时第一个括号“只能暂时靠边,而迫切等待与第二个括号相匹配的第七个括号“)“的出现,类似地,因等来的是第三个括号“,其期待匹配的程度较第二个括号更急迫,则第二个括号也只能靠边,让位于第三个括号,在接受了第四个括号之后,第三个括号的期待得到满足,消解之后,第二个括号的期待匹配就成为当前最急迫的任务了,依次类推。,分析可能出现的不匹配的情况:,到来的右括弧并
15、非是所“期待”的;/左右括号不匹配,例如:考虑下列括号序列: ( ) ) )1 2 3 4 5 6 7 8,到来的是“不速之客”,即多余的右括号;/缺乏左括号,直到结束,也没有到来所“期待”的括弧。/缺乏右括号,算法的设计思想:,1)凡出现左括弧,则进栈;,2)凡出现右括弧,首先检查栈是否空,若栈空,则表明该“右括弧”多余,即缺乏左括号,否则栈顶元素出栈,与输入字符比较,若不匹配则退出;,3)表达式输入结束时,若栈空,则表明表达式中匹配正确, 否则表明“左括弧”有余,即缺乏右括号。,void check() / 对于输入的任意一个字符串,检验括号是否配对InitStack(s); / 初始化栈
16、gets(ch); p=ch; / p指向字符串的首字符while(*p) / 没到串尾switch(*p) case (: Push(s,*p+); / 左括号入栈,且p+ break;case ): if(!StackEmpty(s) / 栈不空Pop(s,e); / 弹出栈顶元素if(!(e=( ,例三、行编辑程序问题,如何实现?,“每接受一个字符即存入存储器” ?,并不恰当!,设立一个输入缓冲区,用以接受用户输入的一行字符,然后逐行存入用户数据区,并假设 “#”为退格符, “”为退行符。,在用户输入一行的过程中,允许 用户输入出差错,并在发现有误时 可以及时更正.,合理的作法是:,假设
17、从终端接受了这样两行字符:whli#ilr#e(s#*s)outchaputchar(*s=#+);,则实际有效的是下列两行:while (*s)putchar(*s+);,算法:获取一个字符;如果它既不是“#”也不是“”则压入栈;如果是“#”,则出栈;如果是“” ,则将栈清空.,while (ch != EOF / 从终端接收下一个字符,ClearStack(S); / 重置S为空栈 if (ch != EOF) ch = getchar();,while (ch != EOF) /EOF为全文结束符,将从栈底到栈顶的字符传送至调用过程的数据区;,回文游戏:顺读与逆读字符串一样(不含空格),
18、1.读入字符串 2.去掉空格(原串) 3.压入栈 4.原串字符与出栈字符依次比较若不等,非回文若直到栈空都相等,回文,字符串:“madam im adam”,表达式求值是程序设计语言编译中一个最基本的问题。它的实现也是需要栈的加入。表达式是由运算对象、运算符、括号组成的有意义的式子。运算符从运算对象的个数上分,有单目运算符和双目运算符,三目运算符;从运算类型上分,有算术运算、关系运算、逻辑运算。,栈的应用-表达式求值,例如: 3*(7 2 )(1)要正确求值,首先了解算术四则运算的规则:a. 从左算到右b. 先乘除,后加减c. 先括号内,后括号外由此,此表达式的计算顺序为:3*(7 2 )=
19、3 * 5 = 15,(2)根据上述三条运算规则,在运算的每一步中,对任意相继出现的算符1和2 ,都要比较优先权关系。 算符优先法所依据的算符间的优先关系见教材P53表3.1(是提供给计算机用的表!),栈的应用(表达式求值),(3)算法的基本思想: 设定两栈:操作符栈 OPTR ,操作数栈 OPND 栈初始化: 设操作数栈 OPND 为空;操作符栈 OPTR 的栈底元素为表达式起始符 #; 依次读入字符:是操作数则入OPND栈,是操作符才则要判断:栈顶元素读入操作符 ,则退栈、计算,结果压入OPND栈; 栈顶元素=读入操作符 且不为#,脱括号(弹出左括号);栈顶元素读入操作符 ,压入OPTR栈
20、。操作符 = #且OPTR的栈顶元素也为#,结束。,OPND的栈底元素即为结果。,栈的应用(表达式求值),举例输入: 3*(7-2)#,Status EvaluateExpression( OperandType /EvaluateExpression,运算符与栈顶比较并查3.1表,判C是否操作符,中缀表达式 后缀表达式a*b+c ab*c+a+b*c abc*+a+(b*c+d)/e abc*d+e/+,例 计算 2+4-3*6#,如何按后缀式进行运算? 可以用两句话来归纳它的求值规则: “先找运算符,后找操作数。“ 请看动画:,运算过程为:对后缀式从左向右“扫描”,遇见操作数则暂时保存,遇
21、见运算符 即可进行运算;此时参加运算的两个操作数应该是在它之前刚刚碰到的两个操 作数,并且先出现的是第一操作数,后出现的是第二操作数。由此可见,在运 算过程中保存操作数的结构应该是个栈。,中缀表达式 后缀表达式a*b+c ab*c+a+b*c abc*+a+(b*c+d)/e abc*d+e/+,例 计算 2+4-3*6 (后缀表达式:2 4 3 6 * - +),栈的应用-迷宫求解,计算机解迷宫时,通常用的是“穷举求解”的方法,即从入口出发,顺某一 方向向前探索,若能走通,则继续往前走;否则沿原路退回,换一个方向再继续探索,直至所有可能的通路都探索到为止,如果所有可能的通路都试探过,还是不能
22、走到终点,那就说明该迷宫不存在从起点到终点的通道 。 请看两个动画:,从演示过程可见:1从入口进入迷宫之后,不管在迷宫的哪一个位置上,都是先往东走, 如果走得通就继续往东走,如果在某个位置上往东走不通的话,就依次试探 往南、往西和往北方向,从一个走得通的方向继续往前直到出口为止;2如果在某个位置上四个方向都走不通的话,就退回到前一个位置,换 一个方向再试,如果这个位置已经没有方向可试了就再退一步,如果所有已 经走过的位置的四个方向都试探过了,一直退到起始点都没有走通,那就说 明这个迷宫根本不通;3所谓“走不通”不单是指遇到“墙挡路”,还有“已经走过的路不能 重复走第二次“,它包括“曾经走过而没
23、有走通的路“。显然为了保证在任何位置上都能沿原路退回,需要用一个“后进先出” 的结构即栈来保存从入口到当前位置的路径。并且在走出出口之后,栈中保 存的正是一条从入口到出口的路径。,由此,求迷宫中一条路径的算法的基本思想是: 若当前位置“可通”,则纳入“当前路径”,并继续朝“下一位置”探索; 若当前位置“不可通”,则应顺着“来的方向”退回到“前一通道块”,然后朝着 除“来向”之外的其他方向继续探索;若该通道块的四周四个方块均“不可通”, 则应从“当前路径”上删除该通道块。“纳入路径”的操作即为“当前位置入栈”; 退回到“前一通道块即为出栈“从当前路径上删除前一通道块“的操作即为“出栈“。,求迷宫
24、中一条从入口到出口的路径的伪码算法如下: 设定当前位置的初值为入口位置; do 若当前位置可通, 则 将当前位置插入栈顶,并留下足迹;/入栈并纳入路径 若该位置是出口位置,则算法结束; / 此时栈中存放的是一条从入口位置到出口位置的路径 否则切换当前位置的相邻方块为新的当前位置;/ 从下一个位置继续探索 ,Pass(curpos),FootPrint(curpos),Nextpos(curpos,e.di),MarkPrint(e.seat),否则 若栈不空且栈顶位置尚有其他方向未被探索, 则设定新的当前位置为: 沿顺时针方向旋转找到的栈顶位置的下一相邻块; 若栈不空但栈顶位置的四周均不可通,
25、 则 删去栈顶位置, 并留下标志 /出栈, 从路径中删去该通道块 若栈不空,则重新测试新的栈顶位置, 直至找到一个可通的相邻块或出栈至栈空; while (栈不空);,数据结构设计,1、迷宫 typedef struct PosType / 迷宫坐标位置类型int x; / 行值int y; / 列值PosType;#define MAXLENGTH 25 / 设迷宫的最大行列为25typedef int MazeTypeMAXLENGTHMAXLENGTH; / 迷宫数组类型行列,2、栈typedef struct SElemType / 栈的元素类型int ord; / 通道块在路径上的序
26、号PosType seat; / 通道块在迷宫中的坐标位置int di; / 从此通道块走向下一通道块的方向(03表示东北)SElemType;,/判断当前位置是否可通Status Pass(PosType b) / 当迷宫m的b点的序号为1(可通过路径),返回OK;否则,返回ERRORif(mb.xb.y=1)return OK;elsereturn ERROR; /留下足迹void FootPrint(PosType a) / 使迷宫m的a点的值变为足迹(curstep)ma.xa.y=curstep; ,void NextPos(PosType ,Status MazePath(PosT
27、ype start,PosType end) / 若迷宫m中存在从入口start到出口end的通道,则求得一条/ 存放在栈中(从栈底到栈顶),并返回TRUE;否则返回FALSEInitStack(S); / 初始化栈 curpos=start; / 当前位置在入口do if(Pass(curpos) / 当前位置可以通过,即是未曾走到过的通道块FootPrint(curpos); / 留下足迹e.ord=curstep; e.seat=curpos; e.di=0;Push(S,e); / 入栈当前位置及状态 curstep+; / 足迹加1if(curpos.x=end.x / 由当前位置及
28、移动方向,确定下一个当前位置 ,else / 当前位置不能通过if(!StackEmpty(S) / 栈不空 Pop(S,e); / 退栈到前一位置 curstep-; / 足迹减1while(e.di=3,递归的实现,函数中有直接或间接调用自身函数的语句这样的函数成为递归函数。 递归函数用的好,可以简化编程工作 但函数自己调用自己有可能造成死循环。 为避免死循环,要做到以下两点: (1)降价。函数的实参值每次是不一样的,一般逐渐减小。(2)有出口。,递归的实现,Ackerman(m,n)=,n+1 m=0 时/出口ack(m,n)= ack(m-1,1) m0 ,n=0 时ack(m-1,a
29、ck(m,n-1) m0,n0 时,int ack(int m,int n)int z;if(m=0)z=n+1; / 出口else if(n=0)z=ack(m-1,1); / 对形参m降阶elsez=ack(m-1,ack(m,n-1); / 对形参m、n降阶return z; ,递归的实现,将所有的实在参数、返回地址等信息传递给被调用函数保存; 为被调用函数的局部变量分配存储区; 将控制转移到被调用函数的入口。,当在一个函数的运行期间调用另一个函数时,在运行该被调用函数之前,需先完成三项任务:,栈的应用:函数的调用栈,例1 递归的执行情况分析,void print(int w) int
30、i;if ( w!=0) print(w-1);for(i=1;i=w;+i)printf(“%3d ”,w);printf(“/n”); ,运行结果: 1 2 2 3 3 3,栈的应用:递归过程及其实现,递归调用执行情况如下:,print(w-1),w=3;,print(w-1);,print(w-1);,输出:1,输出:2 2,返回,i=13, print(w),i=12,print(w),i=11, print(w),输出:3 3 3,void print(int w) int i;if ( w!=0) print(w-1);for(i=1;i=w;+i)printf(“%3d ”,w)
31、;printf(“/n”); ,汉诺塔问题,现有命名为A,B,C的塔座,A塔座上从下到上按金字塔状叠放着n个不同大小的圆盘,编号为1,2,3n。现在把所有盘子一个一个移动到塔座C上,并且每次移动同一根塔座上都不能出现大盘子在小盘子上方,请问至少需要多少次移动,设移动次数为H(n)。移动时可以用塔座B做辅助塔座。 首先我们肯定是把上面n-1个盘子移动到柱子B上,然后把最大的一块放在C上,最后把B上的所有盘子移动到C上。,void hanoi(int n,char A,char C,char B) / 将塔座A上按直径由小到大且自上而下编号为1至n的n个圆盘/ 按规则搬到塔座B上。C可用作辅助塔座if(n=1) / (出口)move(A,1,B); / 将编号为的圆盘从A移到Belsehanoi(n-1,A,B,C); / 将A上编号为1至n-1的圆盘移到C,B作辅助塔(降阶递归调用)move(A,n,B); / 将编号为n的圆盘从A移到Bhanoi(n-1,C,A,B); / 将C上编号1为至n-1的圆盘移到B,A作辅助塔(降阶递归调用),