数据结构第三章 栈和队列.docx
- 文档编号:7926728
- 上传时间:2023-01-27
- 格式:DOCX
- 页数:25
- 大小:56.90KB
数据结构第三章 栈和队列.docx
《数据结构第三章 栈和队列.docx》由会员分享,可在线阅读,更多相关《数据结构第三章 栈和队列.docx(25页珍藏版)》请在冰豆网上搜索。
数据结构第三章栈和队列
第三章栈和队列
【学习目标】
1.掌握栈和队列这两种抽象数据类型的特点,并能在相应的应用问题中正确选用它们。
2.熟练掌握栈类型的两种实现方法。
3.熟练掌握循环队列和链队列的基本操作实现算法。
4.理解递归算法执行过程中栈的状态变化过程。
【重点和难点】
栈和队列是在程序设计中被广泛使用的两种线性数据结构,因此本章的学习重点在于掌握这两种结构的特点,以便能在应用问题中正确使用。
【知识点】
顺序栈、链栈、循环队列、链队列
【学习指南】
在这一章中,主要是学习如何在求解应用问题中适当地应用栈和队列,栈和队列在两种存储结构中的实现都不难,但应该对它们了如指掌,特别要注意它们的基本操作实现时的一些特殊情况,如栈满和栈空、队满和队空的条件以及它们的描述方法。
本章要求必须完成的算法设计题为:
3.15,3.17,3.19,3.22,3.28,3.30,3.31,3.32。
其中前4个主要是练习栈的应用,后4个主要是有关队列的实现方法的练习。
【课前思考】
1.什么是线性结构?
简单地说,线性结构是一个数据元素的序列。
2.你见过餐馆中一叠一叠的盘子吗?
如果它们是按1,2,…,n的次序往上叠的,那么使用时候的次序应是什么样的?
必然是依从上往下的次序,即n,…,2,1。
它们遵循的是"后进先出"的规律,这正是本章要讨论的"栈"的结构特点。
3.在日常生活中,为了维持正常的社会秩序而出现的常见现象是什么?
是"排队"。
在计算机程序中,模拟排队的数据结构是"队列"。
[引言]
栈和队列是两种操作受限的线性表,是两种常用的数据类型
通常称,栈和队列是限定插入和删除只能在表的“端点”进行的线性表。
线性表栈队列
Insert(L,i,x)Insert(S,n+1,x)Insert(Q,n+1,x)
1≤i≤n+1
Delete(L,i)Delete(S,n)Delete(Q,1)
1≤i≤n
即线性表允许在表内任一位置进行插入和删除;而栈只允许在表尾一端进行插入和删除;队列只允许在表尾一端进行插入,在表头一端进行删除。
3.1栈
3.1.1栈的类型定义
栈(Stack)是限定只能在表的一端进行插入和删除操作的线性表。
在表中,允许插入和删除的一端称作"栈顶(top)",不允许插入和删除的另一端称作"栈底(bottom)"。
其类型定义如下:
ADTStack{
数据对象:
D={ai|ai∈ElemSet,i=1,2,...,n,n≥0}
数据关系:
R1={
基本操作:
InitStack(&S)
操作结果:
构造一个空栈S。
DestroyStack(&S)
初始条件:
栈S已存在。
操作结果:
栈S被销毁。
ClearStack(&S)
初始条件:
栈S已存在。
操作结果:
将S清为空栈。
StackEmpty(S)
初始条件:
栈S已存在。
操作结果:
若栈S为空栈,则返回TRUE,否则返回FALSE。
StackLength(S)
初始条件:
栈S已存在。
操作结果:
返回栈S中元素个数,即栈的长度。
GetTop(S,&e)
初始条件:
栈S已存在且非空。
操作结果:
用e返回S的栈顶元素。
Push(&S,e)
初始条件:
栈S已存在。
操作结果:
插入元素e为新的栈顶元素。
Pop(&S,&e)
初始条件:
栈S已存在且非空。
操作结果:
删除S的栈顶元素,并用e返回其值。
StackTraverse(S,visit())
初始条件:
栈S已存在且非空,visit()为元素的访问函数。
操作结果:
从栈底到栈顶依次对S的每个元素调用函数visit(),一旦visit()失败,则操作失败。
}ADTStack
3.1.2栈的存储表示和操作的实现
和线性表类似,栈也有两种存储表示,其顺序存储结构简称为顺序栈。
一、顺序栈类型的定义
//结构定义:
typedefstruct{
ElemType*base;//存储空间基址
inttop;//栈顶指针
intstacksize;//允许的最大存储空间以元素为单位
}Stack;
和顺序表类似,对顺序栈也需要事先为它分配一个可以容纳最多元素的存储空间,base为这个存储空间的基地址,也即一维数组的地址。
从名称来讲,"栈顶指针"意为指示栈顶元素在栈中的位置,但它的值实际是栈中元素的个数,和顺序表中的length值的意义相同。
为了应用方便,这个"最大空间的容量"应由使用这个顺序栈的程序员决定,它的默认值和顺序表的默认值相同。
用图表示顺序栈如下:
图中的顺序栈的最大容量为7,当前栈中元素个数为4,因此,我们也可认为栈顶指针总是指在栈顶元素的后面一个位置上。
//基本操作接口(函数声明):
voidInitStack(Stack&S,intmaxsize);
// 构造一个最大存储容量为maxsize的空栈S。
voidDestroyStack(Stack&S);
// 销毁栈S,S不再存在。
voidClearStack(Stack&S);
// 将S置为空栈。
boolStackEmpty(StackS);
// 若栈S为空栈,则返回TRUE,否则返回FALSE。
intStackLength(StackS);
// 返回S的元素个数,即栈的长度。
boolGetTop(StackS,ElemType&e);
// 若栈不空,则用e返回S的栈顶元素,并返回TRUE;否则返回FALSE。
boolPush(Stack&S,ElemTypee);
// 若栈的存储空间不满,则插入元素e为新的栈顶元素,并返回TRUE;
// 否则返回FALSE。
boolPop(Stack&S,ElemType&e);
// 若栈不空,则删除S的栈顶元素,用e返回其值,并返回TRUE;否则返回FALSE。
voidStackTraverse(StackS,void(*visit(ElemType))
// 依次对S的每个元素调用函数visit(),一旦visit()失败,则操作失败。
在此只给出其中4个函数的定义。
对顺序栈来说,空栈的初始化和顺序表的初始化完全相同。
这不奇怪,因为它们的结构是一样的。
也就是说,并非对栈而言取不到除栈顶之外的元素,而是对栈类型来说,不允许这种操作。
voidInitStack(Stack&S,intmaxsize)
{
//构造一个最大存储容量为maxsize的空栈S
if(maxsize==0)
maxsize=MAXLISTSIZE;
S.base=newSElemType[maxsize];
if(!
S.base)exit
(1); //存储分配失败
S.stacksize=maxsize;
S.top=0; //空栈中元素个数为0
}
在此定义中,栈顶指针的值恰为当前栈中元素个数,栈顶指针的初值为0和栈中元素个数为0是同一个含义。
boolGetTop(StackS,ElemType&e)
{
//若栈不空,则用e返回S的栈顶元素,并返回TRUE;否则返回FALSE
if(S.top==0)returnFALSE;
e=*(S.base+S.top-1); //返回非空栈中栈顶元素
returnTRUE;
}
*(S.base+S.top-1)是S.base[S.top-1]的另一种写法,其实质相同。
boolPush(Stack&S,ElemTypee)
{
//若栈的存储空间不满,则插入元素e为新的栈顶元素,
//并返回TRUE;否则返回FALSE
if(S.top==S.stacksize) //栈已满,无法进行插入
returnFALSE;
*(S.base+S.top)=e; //插入新的元素
++S.top; //栈顶指针后移
returnTRUE;
}
boolPop(Stack&S,ElemType&e)
{
//若栈不空,则删除S的栈顶元素,用e返回其值,
//并返回TRUE;否则返回FALSE
if(S.top==0)returnFALSE;
e=*(S.base+S.top-1); //返回非空栈中栈顶元素
--S.top; //栈顶指针前移
returnTRUE;
}
显然,顺序栈的基本操作的时间复杂度,除"遍历"之外,均为常量级的,即O
(1)。
3.2栈的应用举例
3.2.1数制转换
十进制数N和其他d进制数的转换是计算机实现计算的基本问题,其解决方法很多,其中一个简单算法基于下列原理:
N=(Ndivd)×d+Nmodd
(其中:
div为整除运算,mod为求余运算)
例如:
(1348)10=(2504)8,其运算过程如动画演示所示:
假设现要编制一个满足下列要求的程序:
对于输入的任意一个非负十进制整数,打印输出与其等值的八进制数。
问题很明确,就是要输出计算过程中所得到的各个八进制数位。
然而从动画演示的计算过程可见,这八进制的各个数位产生的顺序是从低位到高位的,而打印输出的顺序,一般来说应从高位到低位,这恰好和计算过程相反。
因此,需要先保存在计算过程中得到的八进制数的各位,然后逆序输出,因为它是按"后进先出"的规律进行的,所以用栈最合适。
算法3.1
voidconversion()
{
//对于输入的任意一个非负十进制整数,打印输出与其等值的八进制数
InitStack(S); //构造空栈
cin>>N; //输入一个十进制数
while(N)
{
Push(S,N%8); //"余数"入栈
N=N/8; //非零"商"继续运算
}//while
while(!
StackEmpty)
{ //和"求余"所得相逆的顺序输出八进制的各位数
Pop(S,e);
cout< }//while }//conversion 你们可能会说,用数组直接实现不也很简单吗? 你可以试一下利用数组重新写这个算法,那么你一定能体会到在这个算法中用栈的好处了。 因栈的引入简化了程序设计的问题,突出了解决问题的根本所在。 而用数组不仅掩盖了问题的本质,还要分散精力去考虑数组下标增减等细节问题。 在以后几个例子中你将会看到,实际利用栈的问题中,入栈和出栈操作大都不是直线式的,而是交错进行的。 3.2.2括弧匹配检验 假设表达式中允许包含两种括号: 圆括号和方括号,其嵌套的顺序随意,如([]())或[([][])]等为正确的匹配,[(])或([]()或(()))均为错误的匹配。 现在的问题是,要求检验一个给定表达式中的括弧是否正确匹配? 检验括号是否匹配的方法可用"期待的急迫程度"这个概念来描述。 即后出现的"左括弧",它等待与其匹配的"右括弧"出现的"急迫"心情要比先出现的左括弧高。 换句话说,对"左括弧"来说,后出现的比先出现的"优先"等待检验,对"右括弧"来说,每个出现的右括弧要去找在它之前"最后"出现的那个左括弧去匹配。 显然,必须将先后出现的左括弧依次保存,为了反映这个优先程度,保存左括弧的结构用栈最合适。 这样对出现的右括弧来说,只要"栈顶元素"相匹配即可。 如果在栈顶的那个左括弧正好和它匹配,就可将它从栈顶删除。 例如考虑下列括号序列: [([][])] 12345678 当计算机接受了第一个括号后,它期待着与其匹配的第八个括号的出现,然而等来的却是第二个括号,此时第一个括号"["只能暂时靠边,而迫切等待与第二个括号相匹配的、第七个括号"]"的出现,类似地,因等来的是第三个括号"[",其期待匹配的程度较第二个括号更急迫,则第二个括号也只能靠边,让位于第三个括号,在接受了第四个括号之后,第三个括号的期待得到满足,消解之后,第二个括号的期待匹配就成为当前最急迫的任务了,…,依次类推。 那么,什么样的情况是"不匹配"的情况呢? 上面列举的三种错误匹配从"期待匹配"的角度描述即为: 1.来的右括弧非是所"期待"的; 2.来的是"不速之客"; 3.直到结束,也没有到来所"期待"的。 这三种情况对应到栈的操作即为: 1.和栈顶的左括弧不相匹配; 2.栈中并没有左括弧等在哪里; 3.栈中还有左括弧没有等到和它相匹配的右括弧。 在以上分析的基础上就可以写出检验括弧匹配的算法了。 但在写算法时要注意: 1."匹配"不是"相等"。 因此你若在遇到左括弧时是将当前这个左括弧进栈的话,那么在遇到右括弧时必须分别不同情况进行判别。 2.和栈顶元素进行比较的前提是栈不为空。 因此你在判别当前出现的右括弧是否和相应左括弧匹配之前要先判别当前栈是否为空。 3."没有等到"即为栈不空的情况。 因此在算法结束之前,要判别栈是否已为空了。 4.此外别忘了使用栈之前一定要进行初始化。 3.2.3迷宫求解问题 你做过迷宫的游戏吗? 你从入口进去之后是如何找到出口的? 如果你是在一点也不了解迷宫结构的情况下去走迷宫的话,显然只能是摸索着前进,比如先往一个方向走,若走不通那就只能退回来再试试另一个方向。 但在走的过程中你一定会有意识地"记住"你已经走过的路,否则你会被困在迷宫中永远也走不出来了,对吗? 那么现在就让我们来看看计算机是如何解迷宫的吧! 计算机解迷宫时,通常用的是"穷举求解"的方法,即从入口出发,顺某一方向向前探索,若能走通,则继续往前走;否则沿原路退回,换一个方向再继续探索,直至所有可能的通路都探索到为止,如果所有可能的通路都试探过,还是不能走到终点,那就说明该迷宫不存在从起点到终点的通道。 先看两个动画演示的例子。 从演示过程可见: 1.从入口进入迷宫之后,不管在迷宫的哪一个位置上,都是先往东走,如果走得通就继续往东走,如果在某个位置上往东走不通的话,就依次试探往南、往西和往北方向,从一个走得通的方向继续往前直到出口为止; 2.如果在某个位置上四个方向都走不通的话,就退回到前一个位置,换一个方向再试,如果这个位置已经没有方向可试了就再退一步,如果所有已经走过的位置的四个方向都试探过了,一直退到起始点都没有走通,那就说明这个迷宫根本不通; 3.所谓"走不通"不单是指遇到"墙挡路",还有"已经走过的路不能重复走第二次",它包括"曾经走过而没有走通的路"。 显然为了保证在任何位置上都能沿原路退回,需要用一个"后进先出"的结构即栈来保存从入口到当前位置的路径。 并且在走出出口之后,栈中保存的正是一条从入口到出口的路径。 由此,求迷宫中一条路径的算法的基本思想是: 若当前位置"可通",则纳入"当前路径",并继续朝"下一位置"探索;若当前位置"不可通",则应顺着"来的方向"退回到"前一通道块",然后朝着除"来向"之外的其他方向继续探索;若该通道块的四周四个方块均"不可通",则应从"当前路径"上删除该通道块。 求迷宫中一条从入口到出口的路径的伪码算法如下: 设定当前位置的初值为入口位置; do{ 若当前位置可通, 则{ 将当前位置插入栈顶; //纳入路径 若该位置是出口位置,则算法结束; //此时栈中存放的是一条从入口位置到出口位置的路径 否则切换当前位置的东邻方块为新的当前位置; } 否则 { 若栈不空且栈顶位置尚有其他方向未被探索, 则设定新的当前位置为: 沿顺时针方向旋转找到的栈顶位置的下一相邻块; 若栈不空但栈顶位置的四周均不可通, 则{删去栈顶位置; //从路径中删去该通道块 若栈不空,则重新测试新的栈顶位置, 直至找到一个可通的相邻块或出栈至栈空; } } }while(栈不空); 栈空则说明没有路径存在; 3.2.4表达式求值问题 你们在以往编制的程序中一定含有表达式,或是算术表达式或是逻辑表达式。 那么你有没有想过编译程序是 如何处理这些表达式的? 你们以后在学习编译原理时将会学到很多处理方法,而我们在此只触及皮毛,目的是为了看看栈在里面能起什么作用。 任何一个表达式都是由操作数(operand)、运算符(operator)和界限符(delimiter)组成,其中,操作数可以是常数也可以是被说明为变量或常量的标识符;运算符可以分为算术运算符、关系运算符和逻辑运算符等三类;基本界限符有左右括弧和表达式结束符等。 为了叙述简洁,在此仅限于讨论只含二元运算符的算术表达式。 可将这种表达式定义为: 表达式: : =操作数运算符操作数 操作数: : =简单变量|表达式 简单变量: : =标识符|无符号整数 即正文中对表达式的定义可解释为: 二元表达式是由(第一)操作数(S1)、运算符(OP)和(第二)操作数(S2)三部分依次联接而成;其中的操作数可以是简单变量,也可以是表达式;而简单变量可以是标识符,也可以是无符号整数。 由于算术运算的规则是: 先乘除后加减、先左后右和先括弧内后括弧外,则对表达式进行运算不能按其中运算符出现的先后次序进行。 那么怎么办? 其中一个方法是先将它转换成另一种形式。 在计算机中,对这种二元表达式可以有三种不同的标识方法。 假设Exp=S1+OP+S2 则称OP+S1+S2为表达式的前缀表示法(简称前缀式) 称S1+OP+S2为表达式的中缀表示法(简称中缀式) 称S1+S2+OP为表达式的后缀表示法(简称后缀式) 可见,它以运算符所在不同位置命名的。 例如: 若 则它的 前缀式为: 中缀式为: 后缀式为: 综合比较它们之间的关系可得下列结论: 1.三式中的"操作数之间的相对次序相同"; 2.三式中的"运算符之间的的相对次序不同"; 3.中缀式丢失了括弧信息,致使运算的次序不确定; 4.前缀式的运算规则为: 连续出现的两个操作数和在它们之前且紧靠它们的运算符构成一个最小表达式; 5.后缀式的运算规则为: ·运算符在式中出现的顺序恰为表达式的运算顺序; ·每个运算符和在它之前出现且紧靠它的两个操作数构成一个最小表达式; 以下就分"如何按后缀式进行运算"和"如何将原表达式转换成后缀式"两个问题进行讨论。 如何按后缀式进行运算? 可以用两句话来归纳它的求值规则: "先找运算符,后找操作数。 " 运算过程为: 对后缀式从左向右"扫描",遇见操作数则暂时保存,遇见运算符即可进行运算;此时参加运算的两个操作数应该是在它之前刚刚碰到的两个操作数,并且先出现的是第一操作数,后出现的是第二操作数。 由此可见,在运算过程中保存操作数的结构应该是个栈。 如何由原表达式转换成后缀式? 先分析一下"原表达式"和"后缀式"两者中运算符出现的次序有什么不同。 例一 原表达式a×b/c×d-e+f 后缀式: ab×c/d×e-f+ 例二 原表达式: a+b×c-d/e×f 后缀式: abc×+de/f×- 例一原表达式中运算符出现的先后次序恰为运算的顺序,自然在后缀式中它们出现的次序和原表达式相同。 但例二中运算符出现的先后次序不应该是它的运算顺序。 按照算术运算规则,先出现的"加法"应在在它之后出现的"乘法"完成之后进行,而应该在后面出现的"减法"之前进行;同理,后面一个"乘法"应后于在它之前出现的"除法"进行,而先于在它之前的"减法"进行。 这可能有点绕嘴,为此我们先引进一个运算符的"优先数"的概念。 给每个运算符赋以一个优先数的值,如下所列: 运算符#(+-×/** 优先数 -1011223 其"**"为乘幂运算,"#"为结束符。 容易看出,优先数反映了算术运算中的优先关系,即优先数"高"的运算符应优先于优先数低的运算符进行运算。 也就是说,对原表达式中出现的每一个运算符是否即刻进行运算取决于在它后面出现的运算符,如果它的优先数"高或等于"后面的运算,则它的运算先进行,否则就得等待在它之后出现的所有优先数高于它的"运算"都完成之后再进行。 显然,保存运算符的结构应该是个栈,从栈底到栈顶的运算符的优先数是从低到高的,因此它们运算的先后应是从栈顶到栈底的。 因此,从原表达式求得后缀式的规则为: 1)设立运算符栈; 2)设表达式的结束符为"#",预设运算符栈的栈底为"#"; 3)若当前字符是操作数,则直接发送给后缀式; 4)若当前字符为运算符且优先数大于栈顶运算符,则进栈,否则退出栈顶运算符发送给后缀式; 5)若当前字符是结束符,则自栈顶至栈底依次将栈中所有运算符发送给后缀式; 6)"("对它之前后的运算符起隔离作用,则若当前运算符为"("时进栈; 7)")"可视为自相应左括弧开始的表达式的结束符,则从栈顶起,依次退出栈顶运算符发送给后缀式直至栈顶字符为"("止。 算法3.2 voidtransform(charsuffix[],charexp[]){ //从合法的表达式字符串exp求得其相应的后缀式suffix InitStack(S);Push(S,#); p=exp;ch=*p; while(! StackEmpty(S)){ if(! IN(ch,OP))Pass(suffix,ch); //直接发送给后缀式 else{ switch(ch){ case(: Push(S,ch);break; case): { Pop(S,c); while(c! =() {Pass(suffix,c);Pop(S,c)} break;} defult: { while(! Gettop(S,c)&&(precede(c,ch))) {Pass(suffix,c);Pop(S,c);} if(ch! =#)Push(S,ch);
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 数据结构第三章 栈和队列 数据结构 第三 队列
![提示](https://static.bdocx.com/images/bang_tan.gif)