栈与队列.docx
- 文档编号:4406081
- 上传时间:2022-12-01
- 格式:DOCX
- 页数:27
- 大小:349.12KB
栈与队列.docx
《栈与队列.docx》由会员分享,可在线阅读,更多相关《栈与队列.docx(27页珍藏版)》请在冰豆网上搜索。
栈与队列
第3章栈与队列
内容概要:
本章介绍两种特殊的线性表:
栈和队列。
包括栈和队列的逻辑结构、存储结构、基本运算、栈和队列在算法设计中的应用等。
教学目标
1.熟练掌握栈的两种存储方法。
2.熟练掌握循环队列和链队列的基本操作实现算法。
3.理解栈或队列的应用。
基本知识点:
顺序栈、链栈、循环队列、链队列。
重点:
栈和队列的特点,顺序栈和链栈上基本运算的实现算法;顺序队列、循环队列和链队上基本运算算法。
难点:
栈与队列各种存住储结构上的操作。
本章知识体系结构:
课时安排:
4个课时
课程
数据结构
教学教具
多媒体课件
学时
2
班级
06网络
教学日期/课时
/2课时
教学单元
第三章栈与队列
教学方法
讲授(PPT)
教学目标
1.掌握抽象数据类型栈的特点,并能在相应的应用问题中正确选用它们。
2.熟练掌握栈类型的两种实现方法。
3.理解递归算法执行过程中栈的状态变化过程。
教学重点
栈的逻辑结构、顺序栈与链栈的实现
教学难点
栈的各种操作
组
织
教
学
一.栈的定义
二.栈的顺序存储结构及其运算
三.栈的链式存储结构及其运算
四.栈的应用举例
五.小结,布置作业
作
业
复习本节内容并预习下节课
课堂情况及
课后分析
课程
数据结构
教学教具
多媒体课件
学时
2
班级
06网络
教学日期/课时
/2课时
教学单元
第三章栈与队列
教学方法
讲授(PPT)
教学目标
1.掌握抽象数据类型队列的特点。
2.熟练掌握循环队列和链队列的基本操作实现算法。
教学重点
队列的逻辑结构、顺序队与链队的实现
教学难点
队列的各种操作
组
织
教
学
一.队列的定义
二.队列的顺序存储结构及其运算
三.队列的链式存储结构及其运算
四.队列的应用举例
五.小结,布置作业
作
业
习题3
课堂情况及
课后分析
3.1栈
3.1.1栈的定义及其运算
栈是只能在表的一端进行插入和删除的线性表。
栈中允许插入和删除的一端称为栈顶(top),另一端是固定端,称为栈底(bottom),如图3-1所示。
由于只允许在栈顶进行插入和删除,所以栈的操作是按“后进先出”的原则进行的,因此,栈又称为后进先出(LastInFirstOut,简称LIFO)的线性表。
当栈中没有数据元素时称为空栈。
在日常生活中,有很多后进先出的例子,比如洗盘子时,把洗干净的盘子一个叠一个地往上放,相当于进栈;取盘子时,若从上面一个接一个地往下拿时,相当于出栈。
再比如,子弹夹也是一种栈的结构,子弹一个个地压入弹夹中,相当于入栈;子弹射出时总是最后压入的子弹最先射出。
假如数据元素入栈次序为A、B、C,出栈次序为A、C、B,其间所进行的操作序列为:
入栈、出栈、入栈、入栈、出栈、出栈。
在程序设计中,函数的调用、递归的实现等都需要栈这样的数据结构。
栈的基本运算有:
(1)初始化栈:
Init_Stack(s)
初始条件:
栈s不存在。
操作结果:
构造了一个空栈。
(2)判断栈空:
Empty_Stack(s)
初始条件:
栈s已存在。
操作结果:
若s为空栈返回1,否则返回0。
(3)判断栈满:
Full_Stack(s)
初始条件:
栈s已存在。
操作结果:
若s为满栈返回1,否则返回0。
(4)入栈:
Push_Stack(s,x)
初始条件:
栈s已存在。
操作结果:
若栈已满,入栈操作失败,返回-1,否则,在栈顶部插入一个新元素x,修改栈顶指针值,操作成功,返回0。
(5)出栈:
Pop_Stack(s)
初始条件:
栈s存在。
操作结果:
若栈空,操作失败,返回-1,否则栈顶指针s减1,从栈顶部删除元素,操作成功,返回0。
(6)读栈顶元素:
Top_Stack(s)
初始条件:
栈s存在。
操作结果:
若栈空,读失败,返回-1,否则,读栈顶元素,返回0。
课堂拓展:
入栈次序为A、B、C、D,是否能得到出栈次序也为A、B、C、D?
能得到A、D、B、C的出栈序列吗?
3.1.2顺序栈
利用顺序存储方式实现的栈称为顺序栈。
类似于顺序表的定义,用一维数组表示栈:
DataTypedata[MAXSIZE],栈底位置可以设置在数组的任一个端,栈顶随着插入和删除而变化,用一个整型变量top作为栈顶的指针,指明当前栈顶的位置。
用C语言定义顺序栈类型如下:
#defineMAXSIZE1024/*顺序栈的最大容量*/
typedefintDataType;/*定义栈中元素类型,可调整*/
typedefstruct
{DataTypedata[MAXSIZE];/*顺序栈*/
inttop;/*栈顶指针*/
}SeqStack;
定义一个指向顺序栈的指针s:
SeqStack*s;
由以上定义可知,top被称作栈顶指针,但它是一个整型变量,其存储的只是栈顶元素在数组中的位置,即数组的下标,而不是具体的物理地址(这一点一定要和C语言中的指针概念加以区别)。
通常,top指向栈中下一个入栈位置,top的取值范围在0~MAXSIZE之间。
假设栈底在低地址端,则当top=0时,表示空栈;当top值为MAXSIZE时,表示栈满。
如图3-2所示顺序栈的结构及栈顶指针变化示意图。
图3-2顺序栈结构及栈顶指针变化示意图
在顺序栈上实现的基本运算,其算法描述如下:
(1)初始化栈
根据C语言的特点及其栈的类型定义,首先建立栈空间,然后初始化栈顶指针。
SeqStack*Init_SeqStack()
{SeqStack*s;
s=(SeqStack*)malloc(sizeof(SeqStack));/*申请栈空间*/
s->top=0;/*设置栈顶指针值*/
returns;
}
(2)判断空栈
intEmpty_SeqStack(SeqStack*s)
{if(s->top==0)return1;/*栈空*/
elsereturn0;/*栈不空*/
}
(3)判断栈满
intFull_SeqStack(SeqStack*s)
{if(s->top==MAXSIZE)return1;/*栈满*/
elsereturn0;
}
(4)入栈
intPush_SeqStack(SeqStack*s,DataTypex)
{if(Full_SeqStack(s))return-1;/*栈满不能入栈*/
else{
s->data[s->top]=x;s->top++;
return0;
}
}
(5)出栈
intPop_SeqStack(SeqStack*s,DataType*x)
{if(Empty_SeqStack(s))return-1;/*栈空,返回-1*/
else{
s->top--;
*x=s->data[s->top];/*栈顶元素存入*x*/
return0;
}
}
(6)取栈顶元素
intTop_SeqStack(SeqStack*s,DataType*x)
{if(Empty_SeqStack(s))return-1;/*栈空,返回-1*/
else{
*x=s->data[s->top-1];
return0;
}
}
(7)置空栈
voidSetNull_SeqStack(SeqStack*s)
{
s->top=0;
}
说明:
·对于顺序栈,入栈时应首先判断栈是否已满,栈满时,不能进行入栈操作,否则会出现空间溢出,引起错误,这种现象称为上溢。
·出栈和读栈顶元素时,先判断栈是否为空,若为空,说明无数据元素可用,不进行操作。
当栈为空时若进行出栈或读栈顶元素时将产生错误,这种现象称作下溢。
3.1.3链栈
用链式存储结构实现的栈称为链栈。
通常链栈用单链表表示,因此其结点结构与单链表结点的结构相同,链栈的类型说明如下:
typedefstructnode
{DataTypedata;/*数据域*/
structnode*next;/*指针域*/
}LinkStack;
LinkStack*top;/*定义栈顶指针*/
因为栈的主要运算是在栈顶插入、删除,将链表的头部看作栈顶操作起来非常方便,如图3-3所示带头结点的链栈示意图。
链栈的栈顶指针就是链表的头指针。
若头结点中的指针域为空表示链栈为空栈,如图3-3(b)所示。
图3-3带头结点的链栈示意图
由于链栈中的结点是动态分配的,可以不考虑上溢,所以无需定义判断栈满的运算。
链栈的基本操作实现如下:
(1)初始化栈
SeqStack*Init_LinkStack()
{LinkStack*s;
s=(LinkStack*)malloc(sizeof(LinkStack));/*申请头结点空间*/
s->next=NULL;/*设置头结点指针域为空*/
returns;/*返回栈顶指针
}
(2)判断栈是否为空
intEmpty_LinkStack(LinkStack*top)
{if(top->next==NULL)return1;/*栈空,返回1*/
elsereturn0;/*栈不空,返回0*/
}
(3)入栈
voidPush_LinkStack(LinkStack*top,DataTypex)
{LinkStack*s;
s=(LinkStack*)malloc(sizeof(LinkStack));
s->data=x;
s->next=top->next;
top->next=s;
}
(4)出栈
intPop_LinkStack(LinkStack*top,DataType*x)
{StackNode*p;
if(Empty_LinkStack(top))return-1;/*栈空,返回-1*/
else{
p=top->next;/*p指向栈顶元素*/
top->next=p->next;/*将p所指结点从链栈中删除,即出栈操作*/
*x=p->data;/*栈顶元素值存入*x中*/
free(p);/*释放p所指结点占用的空间*/
return0;/*操作成功,返回0*/
}
}
(5)栈置空操作
链栈置空操作要考虑到,链栈是否为空,若链栈不为空,则出栈,直到链空为止。
voidInit_LinkStack(LinkStack*s)
{DataTypex;
while(s->next!
=NULL)
Pop_LinkStack(LinkStack*s,DataType*x);
}
(6)取栈顶元素
intTop_LinkStack(LinkStack*s,DataType*x)
{if(Empty_LinkStack(s))return-1;/*栈空,返回-1*/
else{
*x=s->next->data;/*读取栈顶数据元素*/
return0;/*成功读取,返回0*/
}
}
3.2栈的应用
由于栈具有“先进后出”的特点,在计算机技术中有着广泛的应用。
1.栈是实现函数的嵌套调用和递归调用的基础
程序运行时,系统允许在一个函数中调用另一个函数,称为函数的嵌套调用。
例如,在函数A中调用了函数B,在函数B中又调用了函数C。
根据嵌套调用规则,每个函数在执行完后应返回到调用它的函数中的下一条语句处继续执行。
所以,当函数C执行结束,应回到函数B中调用函数C的下一条语句处继续执行,当函数B执行结束后应回到函数A中调用函数B的下一条语句处继续执行。
如上所述,函数调用的次序与返回的次序正好相反,栈正好满足这种调用机制。
当函数被调用时,系统将该函数的有关信息(地址、参数、局部变量等状态)保存在栈中(入栈),称为保护现场;函数调用完返回时,取出保存在栈中的信息(出栈),称为恢复现场,使程序得以继续运行。
由此可知,系统需实现嵌套调用或递归调用时,设立一个栈结构是必不可少的。
2.利用栈实现迷宫的求解
实验心理学中的一个经典问题:
在一个无顶盖的大盒子中设置很多隔板,并留有一个入口和一个出口,使其形成一个由入口到出口的迷宫。
为了吸引老鼠在迷宫中寻找通路以到达出口,心理学家在迷宫的出口处放了一块奶酪,然后将一只老鼠从入口处赶进迷宫。
从入口到出口的一条通路即为一个解。
求解思想是采用回溯法,回溯法是一种不断试探且及时纠正错误的搜索方法。
从入口出发,按某一方向向前探索,若能走通(未走过的),即某处可以到达,则到达新点,否则试探下一方向;若所有的方向均没有通路,则沿原路返回前一点,换下一个方向再继续试探,直到所有可能的通路都探索到,或找到一条通路,或无路可走又返回到入口点。
在求解过程中,为了保证在到达某一点后不能向前继续行走(无路)时,能正确返回前一点以便继续从下一个方向向前试探,则需要用一个栈保存所能够到达的每一点的下标及从该点前进的方向。
3.有关栈的应用例子十分多,比如数制转换问题、表达式求值问题等,下面通过两个例子加以说明。
例3-1数制转换问题:
将十进制数N转换为d进制的数,其转换方法利用除制取余法:
以N=2327,d=8为例,其转换过程如下:
NN/8(整除)N%8(求余)
23272907
290362
3644
404
十进制数2327转换为八进制数为4427。
图3-4十进制数转换为八进制算法流程图
从上面的转换过程可以看到,在除8求余过程中,最先求出的余数是八进制的最低位,最后求出的余数是八进制的最高位,正好符合栈结构的特点,将求出的余数逐个存入堆栈,最后从栈中弹出的结果就是八进制数。
如图3-4所示十进制转换为八进制的算法流程图。
假定从键盘输入n的值是大于0的整数。
算法描述如下:
(1)初始化栈;
(2)输入一个十进制数n;
(3)如果n等于0,转(6);
(4)将n%8压入栈中;
(5)将n除以8的商赋值于n,转(3);
(6)如果栈是空栈,转(8);
(7)出栈,并输出出栈的值,转(6);
(8)结束。
例3-2表达式求值。
表达式求值是源程序编译中一个最基本的问题。
若要把一个含有表达式的赋值语句翻译成能正确求值的机器语言,首先应正确地解释表达式。
例如表达式
x=9-2*2+5(3-1)
其正确的计算结果应该是10,但若编译程序在对表达式进行处理时,使用简单的从左到右扫描的原则进行计算,则出现错误的计算结果:
9-2*2+5=7*2+5=14+5=19
出错的原因是没有考虑到运算符的优先级。
从(3-1)式可以看出,运算符在两个操作数中间,我们把这样的表达式称为中缀表达式。
在中缀表达式中,运算符具有不同的优先级,并且圆括号还可以改变运算符运算次序,这两点使得运算规律较复杂,求值过程不能简单地从左到右按顺序进行。
因此,后缀表达式就产生了。
将运算符写在两个操作数之后的表达式称为后缀表达式。
后缀表达式中没有括号,而且运算符没有优先级。
后缀表达式的求值过程能够严格地从左到右按顺序进行,符合运算器的求值规律。
例如将上式(3-1)转化为后缀表达式如下:
922*-5+(3-2)
从左到右按顺序运算时,遇到运算符时,则对它前面的两个操作数求值,图3-5是式(3-2)的求值过程示意图。
图3-5后缀表达式的求值过程
由上所述,在表达式求值时,分两步进行:
首先将中缀表达式转换为后缀表达式,再求后缀表达式的值。
为简化问题,本例对整型表达式求值,输入字符串为合法的中缀表达式,表达式由双目运算符+、-、*和圆括号()组成,操作数均为1位十进制数。
第一步:
将中缀表达式转换为后缀表达式
对于合法的中缀表达式,其运算符的优先级从高到低依次为:
“(”、“*”、“+”、“-”、“)”。
其中“+”和“-”为同级运算符,其运算规则为从左到右按顺序运算。
对于多层括号,由内向外进行。
在中缀表达式中,当前看到的运算符不能立即参与运算。
例如式(3-1)中,第一个出现的运算符是-,此时另一个操作数没有出现,而且后出现的*运算符的优先级较高,应该先运算,所以不能先进行-运算,必须将-运算符保存起来。
式(3-1)中-、*的出现次序与实际运算次序正好相反,因此,将中缀表达式转换为后缀表达式时,运算符的次序可能改变,必须设立一个栈来存放运算符。
假定“#”为输入表达式的结束符,则转化过程描述如下:
(1)用“#”规定为表达式的结束符,其运算优先级最低,将其入栈;
(2)从左到右对中缀表达式进行扫描,每次处理一个字符;
(3)若遇到数字,原样输出;
(4)若遇到的运算符“)”,则判断栈顶是否为“(”,若不是出栈并输出,直到栈顶为“(”为止,然后“(”出栈,转(6);
(5)若遇到的是其他运算符,如果栈顶为“(”,则入栈,否则如果它的优先级比栈顶数据元素的优先级高,则直接入栈,比栈顶数据元素的优先级低或相等则栈顶数据元素出栈且输出,直到栈顶数据元素的优先级比它低,然后将它入栈;
(6)重复以上
(2)至(5),直至表达式结束,即遇到表达式结束符“#”;
(7)栈中除“#”以外的数据元素出栈,并输出。
最后“#”出栈。
将中缀表达式3*(4+2)*2-5转换为后缀表达式时,运算符栈状态的变化情况如图3-6所示。
第二步:
后缀表达式求值
由于后缀表达式没有括号,且运算符没有优先级,因此求值过程中,当运算符出现时,只要取得前两个操作数就可以立即进行运算。
而当两个操作数出现时,却不能立即求值,必须先保存等待运算符。
所以后缀表达式的求值过程中也必须设立一个栈,用于存放操作数。
后缀表达式求值算法描述如下:
图3-6将中缀表达式变为后缀表达式时运算符栈状态的变化情况
(1)从左到右对后缀表达式字符进行扫描,每次处理一个字符;
(2)若遇到数字,转化为整数,入栈;
(3)若遇到运算符,从栈中弹出两个数值进行运算,运算结果再入栈;
(4)重复以上步骤,直至表达式结束,栈中最后一个数据元素就是所求表达式的结果。
在后缀表达式342+*2*5-的求值过程中,操作数栈的变化情况如图3-7所示。
图3-7后缀表达式求值过程中数据栈状态的变化情况
3.3队列
3.3.1队列的定义及基本运算
插入和删除操作分别在两端进行的线性表称为队列(queue)。
向队列中插入元素的过程称为入队(enqueue),删除元素的过程称为出队(dequeue)。
允许入队的一端称为队尾(rear),允许出队的一端称为队头(front)。
标识队头和队尾当前位置的变量称为队头指针和队尾指针。
当队列中没有数据元素时称作空队列。
若给定队列q=(a1,a2,a3,a4,…,an),则称a1是队头元素,an是队尾元素,如图3-9所示。
图3-9队列
显然,队列也是一种运算受限制的线性表,由于队列中出队的数据元素一定是队列中最早入队的数据元素,所以队列又称为“先进先出表”(FirstInFirstOut,简称FIFO)。
在日常生活中队列的例子有很多,如排队购物、门诊挂号等。
在队列上进行的基本操作有:
(1)队列初始化:
Init_Queue(q)
初始条件:
队q不存在。
操作结果:
构造了一个空队。
(2)入队操作:
In_Queue(q,x)
初始条件:
队q存在。
操作结果:
对已存在的队列q,在队尾插入一个元素x,队发生变化。
(3)出队操作:
Out_Queue(q,x)
初始条件:
队q存在且非空。
操作结果:
删除队头元素,并返回其值,队发生变化。
(4)读队首元素:
Read_Queue(q,x)
初始条件:
队q存在且非空。
操作结果:
读队首元素,并返回其值,队不变。
(5)判队空操作:
Empty_Queue(q)
初始条件:
队q存在。
操作结果:
若q为空队则返回为1,否则返回为0。
3.3.2顺序队列
与线性表、栈类似,队列也有顺序和链式两种存储结构。
顺序存储结构的队列称为顺序队列。
链式存储结构的队列称作链式队列。
队列的顺序存储结构就是定义一组连续的存储空间存放队列的数据元素。
因为队列的队头和队尾都是活动的,因此,在定义顺序队列类型时应定义队列的队头、队尾两个指针。
顺序队的类型定义如下:
#defineMAXSIZE1024/*队列的最大容量*/
typedefstruct
{DataTypedata[MAXSIZE];/*队员的存储空间*/
intrear,front;/*队头、队尾指针*/
}SeqQueue;
SeqQueue*seq;
我们规定,队列初始化时,front=rear=0,队头指针front始终指向当前的队头元素,而队尾指针rear始终指向数据元素要插入的位置。
每当有数据元素加入时,将该数据元素插入到rear所指的位置,并将rear向后移动一个位置;出队时,删去front所指的元素,并将front向后移动一个位置且返回被删元素。
当front和rear相等时,表示队列为空。
图3-10描述了这一过程。
图3-10顺序队操作示意图
当队列为空时进行出队操作将产生下溢,当队列满时进行入队操作则产生上溢。
另外,顺序队列中还存在“假上溢”现象。
因为在入队和出队操作中,头指针和尾指针只增加不减少,经过一系列的操作后,这样就出现了图3-10(d)中的现象:
数据元素d入队后,队尾指针已经移到了最后,再有数据元素e入队时就会出现溢出,而事实上此时队中并未真的“满员”,这种现象为“假上溢”。
解决假上溢的方法之一是将队列的数据区data[0..MAXSIZE-1]看成头尾相接的循环结构,头尾指针的关系不变,将其称为循环队列,图3-11是循环队列的示意图。
从图中可以看到,当数据元素入队或出队时,front或rear指针值要修改,当front或rear指向MAXSIZE-1单元时,下一个要指的单元为0,该操作可用C语言中的模运算(取余数运算)来实现。
当入队时,队尾指针的修改操作为:
seq->rear=(seq->rear+1)%MAXSIZE;
出队时,队头指针的操作修改为:
seq->front=(seq->front+1)%MAXSIZE;
从图3-11还可以看出,当队空或队满时,front和rear都指向同一个单元,即seq->front=seq->rear,那么如何判断队空和队满呢?
解决方法之一:
增加一个变量num,记录队列中的元素个数,当num等于0时表示队空,当num等于MAXSIZE时,表示队满。
解决方法之二:
少用一个元素空间,即队列大小为MAXSIZE个单元,仅用MAXSIZE-1个单元存放数据,rear指向的单元总是空白的,当seq->front等于seq->rear时,表明队空;当(seq->rear+1)%MAXSIZE等于seq->front时,表时队满,(队满时,存放的元素个数为MAXSIZE-1个)。
解决方法之
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 队列
![提示](https://static.bdocx.com/images/bang_tan.gif)