第三章 栈和队列.docx
- 文档编号:30193700
- 上传时间:2023-08-07
- 格式:DOCX
- 页数:28
- 大小:98.40KB
第三章 栈和队列.docx
《第三章 栈和队列.docx》由会员分享,可在线阅读,更多相关《第三章 栈和队列.docx(28页珍藏版)》请在冰豆网上搜索。
第三章栈和队列
第三章栈和队列
引入:
栈是线性表的子类或受限的线性表。
(不受限的一般线性表操作情形)
头部或尾部尾部或头部
线性表的可能扩展情形:
3-1栈(stack)的概念
3-1-1现实生活中的例子
例1串碟问题
例2铁路调度问题
例3死胡同问题(单人行)
例4饼干筒问题
例5弹夹中装子弹问题
思考题:
再想若干与此相似的例子。
3-1-2栈的特点
push(入栈/压入)pop(出栈/弹出)
栈顶元素
栈顶(Top)
栈中的一般元素
栈底(Bottom)
栈底元素
图3-1栈结构示意图
仅在线性表的尾部进行插入、删除操作的线性表。
如图3-1所示:
3-1-3相关术语
LIFO(LastInFirstOut)----后进先出(或FILO)。
栈顶(Top)、栈底(Bottom)、栈顶元素(TopElement)、栈底元素(BottomElement)。
入栈或压栈或压入(Push)、出栈或弹出(Pop)。
3-1-4栈结构的抽象数据类型定义
设S是一个栈结构,e表示栈中元素,则其抽象数据类型定义如下:
ADTStack{
数据对象:
数据关系:
,其中
为栈顶,
为栈底。
基本操作:
InitStack(&S)----构造空栈S。
//栈结构通过指针型参数S返回。
DestroyStack(&S)----销毁栈S。
ClearStack(&S)----清空栈S。
StackEmpty(S)----若S为空栈,则返回TRUE,否则返回FALSE。
Push(&S,e)----压栈。
//将元素e压入S的当前栈顶。
Pop(&S,&e)----出栈。
//将栈S中栈顶元素弹出并放入e中。
GetTop(S,&e)----获得栈顶元素e。
但栈顶位置不变。
StackLen(S)----返回栈S中元素的个数,或栈的长度。
StackTraverse(S,visit())----从栈底到栈顶,依次对每个元素用visit()处理,一旦visit()失败,则操作失败。
}ADTStack
注:
栈中数据元素的类型根据应用的需要具体定义。
3-1-5栈结构的表示与实现方法
3-1-5-1顺序栈的存储结构
(1)基本思想:
用一组连续的存储单元,依次存放栈之元素。
设指向栈顶的元素之指针为Top,指向栈底的元素指针为Bottom。
如图3-2所示。
a[Top]
a[1]
a[0]base
Bottom=0
图3-2顺序栈的存储结构
(2)栈的基本结构描述
typedefstruct{
SElemType*base;//Bottom指针
SElemType*top;//Top指针
intstackSize;//当前栈的大小
}sqStack;
(3)其它假设
其中,可以设置两个基本的常量如下:
STACK_INIT_SIZE----栈的初始大小。
表示对栈存储结构进行初始分配空间的数据元素个数。
STACKINCREMENT----栈的增量步长。
表示对栈的存储空间进行一次增、减时的元素个数。
Top所指的位置是栈顶元素的下一相邻单元。
因此,在此假设下,进栈和出栈的栈顶元素操作如下:
进栈:
a[Top]=e;Top+=1;…
出栈:
e=a[Top-1];Top--;…
3-1-5-2顺序栈的实现算法描述
顺序栈的ADT定义(见教材P46-47)。
(1)空栈判定方法
StatusStackEmpty(SqStackS)//栈为空返回TRUE,否则返回FALSE
{
if(S.top==S.base)returnTRUE;
elsereturnFALSE;
}//StackEmpty
(2)获取栈顶元素的方法
StatusGetTop(SqStackS,SelemType&e)
{//若栈不空,栈顶元素存入e,返回OK。
否则,返回ERROR。
if(S.top==S.base)returnERROR;//栈空时返回ERROR
e=*(S.top-1);
returnOK;
}//GetTop
(3)压栈操作方法
StackPush(SqStack&S,SelemTypee)
{
if((S.top-S.base)>=S.stackSize)//栈满处理:
增加新单元
{
S.base=(ElemType*)realloc(S.base,(S.stackSize+STACKINCREMENT)*\
sizeof(ElemType));
if(!
S.base)exit(OVERFLOW);//重新分配时失败
S.top=S.base+S.stackSize;//重新设置栈顶及栈的大小信息
S.stackSize+=STACKINCREMENT;
}
*S.top++=e;
returnOK;
}//Push
(4)出栈操作方法
StackPop(SqStack&S,SelemType&e)
{
if(S.top==S.base)returnERROR;//栈空处理
e=*--S.top;
returnOK;
}//Pop
(5)栈的初始化方法
1)顺序分配长度为STACK_INIT_SIZE的空间;其中单元空间的大小由类型ElemType决定。
S.base为空间之底部地址。
2)S.top=S.base;//置为空栈
3)S.stackSize=STACK_INIT_SIZE;//记录栈的大小
4)返回栈结构的指针值&S;//sqStackS类型
(6)其它基本操作的实现方法(作为课外作业和上机作业)
3-1-5-3栈结构的实现方法的进一步讨论
栈结构的具体实现可以用数组的方法、简单链表的方法和教材中的方法。
以下分别加以讨论:
1)数组的方法
设top,base为a数组的下标数据类型,数组的大小为a[N]。
则:
栈的初始状态为top=base=0;
栈满状态为top=N;
栈空状态为top==0或top==base;
其它操作类似。
优点:
直接采用程序语言内部支持的简单数据结构表示栈结构。
缺点:
栈的大小不能改变,必须预先分配好。
空间利用率不高,且不灵活。
关键操作:
Pop():
{top--;e=a[top];}
Push():
{a[top]=e;top++;}
GetTop():
{e=a[top-1];}
2)链表的方法
设top,base为结点类型的指针,在此结构下栈的大小是可变的。
栈的初始状态为top=base=NULL;
栈满状态时表示整个可用内存空间已用完;
栈空表示为top==base;
其它操作类似,但Push()和Pop()操作时,要注意内存空间的及时申请与回收。
优点:
内存空间分配时不用分配多余的空间预留。
栈的大小是可变的。
缺点:
每一次Push()或Pop()都要申请或释放空间,操作太频繁。
关键操作:
Pop():
{top1=top;top=top->next;e=top->data;free(top1);}
Push():
{top1=top;top->data=e;top=new();top->next=top1;}
GetTop():
{e=(top->next)->data;}
3)教材上的方法
采用SqStack,SelemType等结构,利用STACKINCREMENT增量控制栈大小的改变步长。
是前面两种方法的折中。
优点:
栈的大小是可改变的,同时,对内存空间的分配与释放频率又是人为可控的,比简单链表的方法好。
缺点:
空间利用率稍次于简单链表法,但好于简单数组法。
关键操作:
见3-1-5-2节算法描述。
思考题:
多个栈同时存在时,如何充分利用有限的内存空间?
课外作业:
以以上三种结构中的一种为准,完成栈结构的基本操作算法,并上机实现之。
3-2栈的应用举例
例3-1数制之间的转换问题(即10#2#)。
规则:
除2取余倒排序。
分析:
转换关系为
。
当
时,打印出d#数。
由于人们书写数时是从高位向低位方向进行(左右),但计算时是从低位向高位方向进行(右左),因此,实现时自然想到用栈结构来描述之。
例如:
(83)10=(1100101)2
其中,除2取余的计算过程可以用将余数进栈的方法实现,而倒排序的过程用出栈的方法实现。
如图3-3所示。
Top输出顺序:
1100101
1183
0141
1020
push0pop010
015
102
1除2取余顺序1
base
图3-3除2取余倒排序的进栈和出栈过程示意图
余数出栈
余数进栈
初始化
输出结果
图3-4使用栈结构实现2#-10#转换的流程图结构
使用栈结构实现2#-10#转换的好处:
简化了程序设计问题。
使问题变成了简单的顺序模块化结构。
如下所示:
课外作业:
1)用其它方法实现这一转换过程,并比较两者的差异
2)栈以不同的方法实现,如数组、链表、可变顺序结构等。
例3-2括号匹配检查问题:
“(”和“)”匹配“[”和“]”嵌套使用时的配对匹配问题。
基本规则:
匹配对象按先内后外原则。
要求:
括号对完全匹配时返回TRUE,否则返回FALSE。
基本思想(分析):
对给定的带括号字符串,按以下规则扫描进行进栈、出栈操作,如果扫描结束时,栈为空则说明给定的字符串中括号是匹配的,否则说明括号不匹配。
进栈、出栈处理规则:
当栈顶括号与当前括号匹配时,栈顶数据或元素或括号出栈,否则,当前括号进栈。
例如:
“[([][])]”的处理过程如图3-5所示。
top“]”“[”“]”“)”“]”
[[
((((
[[[[[top
basebase
图3-5括号匹配检查问题的进栈和出栈过程操作示意图
课外作业:
画出本例问题的以栈为数据结构的程序处理流程图,并上机实现之。
例3-3行编辑程序问题。
假设:
按字符行接受输入,并存入数据区。
其中,‘#’表示退格符号(即表示前一输入符号是无效字符);‘@’表示退行符号(即表示当前行已有的输入内容无效)。
例如:
whil##ilr#e(s#*s)while(*s)
outcha@putchar(*s=#++)putchar(*s++)
实现思想:
设用一个N行的字符数组pp[N]存放未经处理的N行原始字符串,pp_ok[N]分别存放处理后的字符串,则循环N次可分别处理每一行。
每一行的处理过程根据假设,可以借助栈结构来完成之。
如图3-6所示。
whli##ilr#e(s#*s))s*(elihwwhile(*s)
basetopbasetop(顺序线性表结构)
(处理之前的pp栈)(处理过后的pp_ok_temp栈)(最后的结果pp_ok)
图3-6行编辑程序借助栈结构处理一行的过程示意图
课外作业:
1)画出本例两个串的进栈、出栈处理过程。
2)以栈为数据结构,给出本例的程序处理流程图,并上机实现之。
例3-4迷宫求解问题:
求迷宫的入口到出口所经过的路径。
问题还可分细为很多:
一是有无到达出口的路径,二是有多少条路径,三是最短路径和最长路径。
本例只解决如何求出一条从入口到出口的一条路径问题。
问题分析:
假设从当前点要向前探索一条路径时,有四个方向(东、南、西、北)分别用1、2、3、4编号表示,则每次探索可以依此顺序进行。
在探索失败后退回时,其表现出的特点是后探索的地方要先退到用栈结构来表示已探索的路径信息。
另外,对迷宫中的任一点,数据结构应当能够表示出:
1)该点是可通路径还是墙面(0和1);2)该点是否走过(但不通)(用-2表示);3)该点已探索过的路径方向(即东、南、西、北)4)标记该点是否是通向出口的点(用2表示)。
最后,对探索过程而言,必须随时知道当前位置,用(x,y)表示。
入口
0
1
2
3
4
5
6
7
8
9
0
1
o
2
3
4
5
6
7
8
o
9
迷宫的存储结构设计分析:
以二维数组a方式表示迷宫问题。
元素的取值类型为:
墙用1表示,通道用0表示,已走过不通用-2表示,已走过且通的用+2表示。
当前正在探索的位置:
curpos(x,y)序对。
已探索过的路径信息结构:
(x,y,探索方向,点的状态值)
探索的方法:
当前路径如果可通,则将当前点入栈:
(curpos,方向,点的状态值)。
否则,试探该点下一方向。
如果当前点4个方向都不通,则取出栈顶元素作为当前点。
重复以上过程。
栈结构:
探索点(x,y),探索方向,点的状态值。
出口
算法过程描述:
do//start----迷宫出口;end----迷宫入口
{
if(当前位置可通)
{
将当前位置插入栈顶;
if(当前位置是maze_out时)程序结束;
else将当前位置的东邻通道作为新的当前位置;
}
else
{
if(栈不空,且栈顶位置的4个方向没有探索完)
{
当前位置的置为栈顶位置的下一相邻通道(东邻)
}
if(栈不空,且栈顶位置的4个方向没有已经探索完)
{
删除栈顶位置元素;
从栈中找一个可通的栈顶位置或出栈直到栈空为止;
}
}
}while(栈不空)
算法的实现描述:
typedefstruct//通道块的位置类型
{
intx;
inty;
}PosType;
typedefstruct//迷宫的数据结构类型
{
intmaze[10][10];
}MazeType;
typedefstruct//栈元素的类型
{
intord;//通道块在路径上的序号
PosTypeseat;//通道块在迷宫中的“坐标位置”
intdi;//通道块走向下一块的“方向序号”
}SelemType;
typedefstruct//栈结构类型定义
{
SElemType*base;
SElemType*top;
intstacksize;
}SqStack;
/***********************************************************/
StatusMazePath(MazeTypemaze,PosTypestart,PosTypeend)
{//如maze中有从start到end的通道,则将其放在栈中,并返回TRUE,否则,返回FALSE
InitStack(S);curpos=start;curstep=1;//初始化
do
{
if(Pass(curpos)){//当前位置未走过,且可以通过
FootPrint(curpos);//在当前位置留下足迹或做标记
e=(curstep,curpos,1);//1表示探索方向为东
Push(S,e);
if(curpos==end)returnTRUE;//成功时返回
curpos=NextPos(curpos,1);//当前位置的东邻作为新的当前位置
curstep++;
}//if
else{//当前位置不能通过时
if(!
StackEmpty(S)){
Pop(S,e);
while(e.di==4&&!
StackEmpty(S)
{//已经到第4个方向,则留下不能通过标志,并后退一步
MarkPrint(e.seat);//对e.seat做不能通过标志,对应的迷宫数组元素值为-2
Pop(S,e);
}//while
if(e.di<4){//当前点还没有试探完时
e.di++;
Push(S,e);
curpos=NextPos(e.seat,e.di);//获取下一方向位置作为当前位置
}//if
}//if
}//else
}while(!
StackEmpty(S));
returnFALSE;
}//MazePath
注1:
上机实践时,maze的定义。
注2:
辅助函数Pass(),FootPrint(),NextPos(),MarkPrint(),Push(),Pop()等的设计与实现。
例3-5表达式求值问题。
(编译器的基本功能)
分析:
(1)表达式的构成:
操作数和操作符构成。
操作符的运算具有优先关系,相同优先关系时从左致右顺次优先。
(2)算术表达式求值的基本规则:
先乘除后加减,先括号内后括号外。
正好与栈的LIFO特征吻合。
+
-
*
/
(
)
#
+
>
>
<
<
<
>
>
-
>
>
<
<
<
>
>
*
>
>
>
>
<
>
>
/
>
>
>
>
<
>
>
(
<
<
<
<
<
=
╳
)
>
>
>
>
╳
>
>
#
<
<
<
<
<
╳
=
操作数和操作符之间的3种优先关系:
:
的优先级低于
。
:
的优先级相同于
。
:
的优先级高于
。
基本算术运算符之间的有限关系表:
注1:
‘+’,‘*’,‘-‘,‘/’均低于‘(‘,但高于‘)’。
注2:
‘#’字符是表达市的结束符号。
表达式由‘#’字符开始,也由‘#’字符结束。
注3:
‘(‘遇‘)’时表示括号内的运算已经结束,‘#’遇‘#’时表示表达式的计算已经结束。
反之,‘)’遇‘(‘时,‘#’遇‘)’以及‘(‘遇‘#’均无优先关系。
一旦遇到这种情形,表明表达式有误。
注4:
可以对各算符赋予不同的优先级数值。
例如:
#
(
**
*
/
+
-
)
0
7
3
2
2
1
1
0
栈的使用方法:
设两个栈OPTR----运算符栈,OPND----操作数栈。
基本思想:
OPND置空,OPTR置元素‘#’为栈底。
依次读入表达式中的字符,如为数字值,则进OPND栈。
若为运算符,则与OPTR栈顶元素比较,如果优先时,则进OPTR,否则,取OPND的操作数运算,结果存入OPND,直到整个表达式计算完毕(即遇到‘#’)。
运算实例:
求“3*(7-2)#”的值(见下表)。
步骤
OPTR
OPND
当前字符
主要操作
1
#
3*(7-2)#
3OPND
2
#
3
*(7-2)#
*OPTR
3
#*
3
(7-2)#
(OPTR
4
#*(
3
7-2)#
7OPND
5
#*(
3,7
-2)#
-OPTR
6
#*(-
3,7
2)#
2OPND
7
#*(-
3,7,2
)#
7-2OPND
8
#*(
3,5
)#
(=):
去括号对
9
#*
3,5
#
3*5OPND
10
#
15
#
遇到#,结束
算法描述:
1)OPND空,OPTR置‘#’
2)读入并处理一个字符P
if(P是操作数)
Push(OPND,P);
else//P是运算符时
{
switch(P与OPTR顶比较)
{
case“>”:
P进OPTR栈;
读入下一字符;
break;
case“<”:
//栈顶优先时
取OPTR栈顶元素OP;
取OPND中的操作数a,b;//如a,b
将aOPb运算的结果送入OPND;
break;
case“=”:
弹出运算符栈OPTR中的栈顶元素;
读入下一字符P;
break;
}
}
3)转2)继续处理
思考题:
请仔细阅读并理解教材P53页EvaluateExpression()算法。
3-3**栈与递归的实现过程
3-3-1递归函数概念
递归函数:
自己调用自己的现象。
如果自己直接调用自己,称为直接递归,如果经过第3者调用自己,称为间接递归。
递归实例:
例1画中画(绘画作品、电视画面等)。
例2山里和尚的故事。
例3数学函数现象,如
Fibonacci数列公式
Ackerman函数公式
例4回旋针图形结构。
例5数型结构的递归(递归描述见教材P118)。
例68皇后问题,Hanoi塔问题等问题的递归。
例7广义表(参见第5章P106)。
注1:
这类函数和问题用递归描述和求解比爹带与循环容易。
注2:
递归的问题都可以化为循环去等价地实现。
3-3-2n阶Hanoi塔问题
ABC
1
32
4
图3-3-24阶Hanoi塔问题的初始状态
问题的描述:
设有3个塔座分别称为A,B,C,在A上有n个直径大小各不相同的圆盘,并从小到大编号(如图示)。
要求将此n圆盘按以下规则移到C上:
1)3塔中的圆盘随时保持上小下大的原则;
2)每次只能移动一个圆盘;
3)每一圆盘都可出现在A,B,C上;
问题的分析:
(FTTB逐步求精与抽象,大问题小问题)
整个问题有下面几个子问题:
(1)A上面的n-1个B;
(2)A最下面的第n个C;
(3)B上的n-1个C;
显然,经过上面的一次分解,
(2)已经可解,
(1)和(3)的问题规模则降为n-1个圆盘问题了。
重复以上过程,总是可以将其简化成一个圆盘问题的!
子问题
(1)可以描述成:
将A上面的n-1个圆盘借助C移到B。
子问题(3)可以描述成将B上n-1个圆盘借助A移到C。
算法描述:
//从递归函数的参数需要涉及圆盘原在塔号,圆盘个数,辅助塔号,目标塔号4个参数。
//塔号的表示用字符类型数据表示,圆盘个数用整型数类型表示
//圆盘的移动动作用move(原塔a,盘号n,目标c)实现,step为步数(初值为0)
//printf(“%i.Movedisk%ifrom%cto%c\n”,++step,n,a,c);
voidHanoi(intn,chara,charb,charc)
{
if(n==1)move(a,1,c)//如果只有一个圆盘,则直接从a移到c
else{
hanoi(n-1,a,c,b);//将a上面n-1个圆盘借助c移到b
move(a,n,c);//将a上的圆盘n移到c
hanoi(n-1,b,a,c);//将b上面n-1个圆盘借助a移到c
}
}//Hanoi
函数调用的实质问题:
f(x,y)
{…
firstElem();
…
return;
}
firstElem()
{…}
main()
{…
f(x,y);
…
ff(z,x,y);
…
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 第三章 栈和队列 第三 队列