数据结构栈和队列.docx
《数据结构栈和队列.docx》由会员分享,可在线阅读,更多相关《数据结构栈和队列.docx(24页珍藏版)》请在冰豆网上搜索。
数据结构栈和队列
栈和队列
从数据结构的角度看:
它们和线性表相同
从数据类型的角度看:
它们和线性表不同
线性表栈队列
Insert(L,i,x)Insert(S,n+1,x)Insert(Q,n+1,x)
(1in+1)
Delete(L,i)Delete(S,n)Delete(Q,1)
(1in)
3.1栈的类型定义
ADTStack{
数据对象:
D={ai|ai∈ElemSet,i=1,2,...,n,n≥0}
数据关系:
R1={|ai-1,ai∈D,i=2,...,n}
约定an端为栈顶,a1端为栈底。
基本操作:
InitStack(&S)
操作结果:
构造一个空栈S。
DestroyStack(&S)
初始条件:
栈S已存在。
操作结果:
栈S被销毁。
ClearStack(&S)
初始条件:
栈S已存在。
操作结果:
将S清为空栈。
StackEmpty(S)
初始条件:
栈S已存在。
操作结果:
若栈S为空栈,则返回TRUE,否则FALE。
StackLength(S)
初始条件:
栈S已存在。
操作结果:
返回S的元素个数,即栈的长度。
GetTop(S,&e)
初始条件:
栈S已存在且非空。
操作结果:
用e返回S的栈顶元素。
Push(&S,e)
初始条件:
栈S已存在。
操作结果:
插入元素e为新的栈顶元素。
Pop(&S,&e)
初始条件:
栈S已存在且非空。
操作结果:
删除S的栈顶元素,并用e返回其值。
}ADTStack
3.2栈的应用举例
例一、数制转换
十进制数N和其他d进制数的转换是计算机实现计算的基本问题,其解决方法很多,其中一个简单算法基于下列原理:
N=(Ndivd)×d+Nmodd
(其中:
div为整除运算,mod为求余运算)
例如:
(1348)10=(2504)8,其运算过程如下:
NNdiv8Nmod8
13481684
168210
2125
202
假设现要编制一个满足下列要求的程序:
对于输入的任意一个非负十进制整数,打印输出与其等值的八进制数。
由于上述计算过程是从低位到高位顺序产生八进制数的各个数位,而打印输出,一般来说应从高位到低位进行,恰好和计算过程相反。
因此,若将计算过程中得到的八进制数的各位顺序进栈,则按出栈序列打印输出的即为与输入对应的八进制数。
voidconversion(){
//对于输入的任意一个非负十进制整数,打印输出
//与其等值的八进制数
InitStack(S);//构造空栈
scanf("%d",N);
while(N){
Push(S,N%8);
N=N/8;
}
while(!
StackEmpty(S)){
Pop(S,e);
printf("%d",e);
}
}//conversion
这是利用栈的后进先出特性的最简单的例子。
在这个例子中,栈操作的序列是直线式的,即先一味地入栈,然后一味地出栈。
也许,有的读者会提出疑问:
用数组直接实现不也很简单吗?
仔细分析上述算法不难看出,栈的引入简化了程序设计的问题,划分了不同的关注层次,使思考范围缩小了。
而用数组不仅掩盖了问题的本质,还要分散精力去考虑数组下标增减等细节问题。
例二、括号匹配的检验
假设表达式中允许包含两种括号:
圆括号和方括号,其嵌套的顺序随意,即([]())或[([][])]等为正确的格式,[(])或([())或(()])均为不正确的格式。
检验括号是否匹配的方法可用"期待的急迫程度"这个概念来描述。
例如考虑下列括号序列:
[([][])]
12345678
分析可能出现的不匹配的情况:
1)到来的右括弧非是所“期待”的;
2)到来的是“不速之客”;
3)直到结束,也没有到来所“期待”的。
statusmatching(string&exp){
//检验表达式中所含括弧是否正确嵌套,若是,则返回
//OK,否则返回ERROR
intstate=1;
while(i<=length(exp)&&state){
swithofexp[i]{
case左括弧:
{Push(S,exp[i]);i++;break;}
case")":
{if(NOTStackEmpty(S)&&GetTop(S)="(")
{Pop(S,e);i++;}
else{state=0}
break;
}
……}
}
if(state&&StackEmpty(S))returnOK
elsereturnERROR;
}
例三、行编辑程序问题
一个简单的行编辑程序的功能是:
接受用户从终端输入的程序或数据,并存入用户的数据区。
每接受一个字符即存入用户数据区”不恰当。
较好的做法是,设立一个输入缓冲区,用以接受用户输入的一行字符,然后逐行存入用户数据区。
允许用户输入出差错,并在发现有误时可以及时更正。
例如,可用一个退格符“#”表示前一个字符无效;可用一个退行符“@”,表示当前行中的字符均无效。
例如,假设从终端接受了这样两行字符:
whli##ilr#e(s#*s)
outcha@putchar(*s=#++);
则实际有效的是下列两行:
while(*s)
putchar(*s++);
voidLineEdit(){
//利用字符栈S,从终端接收一行并传送至调用过程
//的数据区。
InitStack(S);//构造空栈S
ch=getchar();//从终端接收第一个字符
while(ch!
=EOF){//EOF为全文结束符
while(ch!
=EOF&&ch!
='\n'){
switch(ch){
case'#':
Pop(S,c);break;
//仅当栈非空时退栈
case'@':
ClearStack(S);break;//重置S为空栈
default:
Push(S,ch);break;
//有效字符进栈,未考虑栈满情形
}
ch=getchar();//从终端接收下一个字符
}
将从栈底到栈顶的字符传送至调用过程的数据区;
ClearStack(S);//重置S为空栈
if(ch!
=EOF)ch=getchar();
}
DestroyStack(S);
}
例四、迷宫求解
求迷宫中从入口到出口的所有路径是一个经典的程序设计问题。
由于计算机解迷宫时,通常用的是“穷举求解”的方法,即从入口出发,顺某一方向向前探索,若能走通,则继续往前走;否则沿原路退回,换一个方向再继续探索,直至所有可能的通路都探索到为止。
为了保证在任何位置上都能沿原路退回,显然需要用一个后进先出的结构来保存从入口到当前位置的路径。
因此,在求迷宫通路的算法中应用“栈”也就是自然而然的事了。
假设迷宫如下图所示:
#
#
#
#
#
#
#
#
#
#
#
#
$
$
$
#
#
#
#
$
$
$
#
#
#
$
$
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#
假设“当前位置”指的是“在搜索过程中某一
时刻所在图中某个方块位置”,则求迷宫中一条路
径的算法的基本思想是:
若当前位置"可通",则纳
入"当前路径",并继续朝“下一位置”探索,即切
换“下一位置”为“当前位置”,如此重复直至到
达出口;若当前位置“不可通”,则应顺着“来向”
退回到“前一通道块”,然后朝着除“来向”之外的其他方向继续探索;若该通道块的四周四个方块均“不可通”,则应从“当前路径”上删除该通道块。
所谓“下一位置”指的是“当前位置”四周四个方向(东、南、西、北)上相邻的方块。
假设以栈S记录“当前路径”,则栈顶中存放的是“当前路径上最后一个通道块”。
由此,“纳入路径”的操作即为“当前位置入栈”;“从当前路径上删除前一通道块”的操作即为“出栈”。
求迷宫中一条从入口到出口的路径的算法可简单描述如下:
设定当前位置的初值为入口位置;
do{
若当前位置可通,
则{将当前位置插入栈顶;//纳入路径
若该位置是出口位置,则结束;
//求得路径存放在栈中
否则切换当前位置的东邻方块为新的当前位置;
}
否则{
若栈不空且栈顶位置尚有其他方向未被探索,
则设定新的当前位置为:
沿顺时针方向旋转
找到的栈顶位置的下一相邻块;
若栈不空但栈顶位置的四周均不可通,
则{删去栈顶位置;//从路径中删去该通道块
若栈不空,则重新测试新的栈顶位置,
直至找到一个可通的相邻块或出栈至栈空;
}
}while(栈不空);
在此,尚需说明一点的是,所谓当前位置可通,指的是未曾走到过的通道块,即要求该方块位置不仅是通道块,而且既不在当前路径上(否则所求路径就不是简单路径),也不是曾经纳入过路径后又从路径上删除的通道块(否则只能在死胡同内转圈)。
typedefstruct{
intord;//通道块在路径上的“序号”
PosTypeseat;//通道块在迷宫中的“坐标位置”
intdi;//从此通道块走向下一通道块的“方向”
}SElemType;//栈的元素类型
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);
Push(S,e);//加入路径
if(curpos==end)return(TRUE);
//到达终点(出口)
curpos=NextPos(curpos,1);
/下一位置是当前位置的东邻
curstep++;//探索下一步
}
else{//当前位置不能通过
if(!
StackEmpty(S)){
Pop(S,e);
while(e.di==4&&!
StackEmpty(S)){
MarkPrint(e.seat);Pop(S,e);
//留下不能通过的标记,并退回一步
}//while
if(e.di<4){
e.di++;Push(S,e);
//换下一个方向探索
curpos=NextPos(curpos,e.di);
//设定当前位置是该新方向上的相邻块
}//if
}//if
}//else
}while(!
StackEmpty(S));
return(FALSE);
}//MazePath
例五、表达式求值
限于二元运算符的表达式定义:
表达式:
:
=(操作数)+(运算符)+(操作数)
操作数:
:
=简单变量|表达式
简单变量:
:
=标识符|无符号整数
在计算机中,表达式可以有三种不同的标识方法
设Exp=S1+OP+S2
则称OP+S1+S2为表达式的前缀表示法
称S1+OP+S2为表达式的中缀表示法
称S1+S2+OP为表达式的后缀表示法
可见,它以运算符所在不同位置命名的。
例如:
Exp=ab+(cd/e)f
前缀式:
+abc/def
中缀式:
ab+cd/ef
后缀式:
abcde/f+
结论:
1)操作数之间的相对次序不变;
2)运算符的相对次序不同;
3)中缀式丢失了括弧信息,致使运算的次序不确定;
4)前缀式的运算规则为:
连续出现的两个操作数和在它们之前且紧靠它们的运算符构成一个最小表达式;
5)后缀式的运算规则为:
●运算符在式中出现的顺序恰为表达式的运算顺序;
●每个运算符和在它之前出现且紧靠它的两个操作数构成一个最小表达式;
●如何从后缀式求值?
先找运算符,后找操作数
●如何从原表达式求得后缀式?
分析“原表达式”和“后缀式”中的运算符:
原表达式:
a+bcd/ef
后缀式:
abc+de/f
运算符#(+/
优先数-1011223
每个运算符的运算次序要由它之后的一个运算符来定,若当前运算符的优先数小,则暂不进行;否则立即进行。
表达式求值的规则:
1)设立运算符栈和操作数栈;
2)设表达式的结束符为“#”,予设运算符栈的栈底为“#”
3)若当前字符是操作数,则进操作数栈;
4)若当前运算符的优先数高于栈顶运算符,则进栈;
5)否则,退出栈顶运算符作相应运算;
6)“(”对它之前后的运算符起隔离作用,“)”可视为自相应左括弧开始的表达式的结束符。
算法描述如下:
OperandTypeEvaluateExpression(){
//算术表达式求值的算符优先算法。
设OPTR和OPND
//分别为运算符栈和运算数栈,OP为运算符集合。
InitStack(OPTR);Push(OPTR,'#');
initStack(OPND);c=getchar();
while(c!
='#'||GetTop(OPTR)!
='#'){
if(!
In(c,OP)){Push((OPND,c);c=getchar();}
//不是运算符则进栈
else
switch(precede(GetTop(OPTR),c){
case'<':
//栈顶元素优先权低
Push(OPTR,c);c=getchar();
break;
case'=':
//脱括号并接收下一字符
Pop(OPTR,x);c=getchar();
break;
case'>'#//退栈并将运算结果入栈
Pop(OPTR,theta);
Pop(OPND,b);Pop(OPND,a);
Push(OPND,Operate(a,theta,b));
break;
}//switch
}//while
returnGetTop(OPND);
}//EvaluateExpression
例六、实现递归
在高级语言编制的程序中,调用函数与被调用函数之间的链接和信息交换必须通过栈例进行。
当在一个函数的运行期间调用另一个函数时,在运行该被调用函数之前,需先完成三件事:
1)将所有的实在参数、返回地址等信息传递给被调用函数保存;
2)为被调用函数的局部变量分配存储区;
3)将控制转移到被调用函数的入口。
从被调用函数返回调用函数之前,应该完成:
1)保存被调函数的计算结果;
2)释放被调函数的数据区;
3)依照被调函数保存的返回地址将控制转移到调用函数。
多个函数嵌套调用的规则是:
后调用先返回
此时的内存管理实行“栈式管理”
递归过程指向过程中占用的数据区,称之为递归工作栈
每一层的递归参数合成一个记录,称之为递归工作记录
栈顶记录指示当前层的执行情况,称之为当前活动记录
栈顶指针,称之为当前环境指针
例如:
voidhanoi(intn,charx,chary,charz)
//将塔座x上按直径由小到大且至上而下编号为1至n
//的n个圆盘按规则搬到塔座z上,y可用作辅助塔座。
1{
2if(n==1)
3move(x,1,z);//将编号为1的圆盘从x移到z
4else{
5hanoi(n-1,x,z,y);//将x上编号为1至n-1的圆
//盘移到y,z作辅助塔
6move(x,n,z);//将编号为n的圆盘从x移到z
7hanoi(n-1,y,x,z);//将y上编号为1至n-1的圆盘
//移到z,x作辅助塔
8}
9}
3.3栈类型的实现
顺序栈
类似于线性表的顺序映象实现,指向表尾的指针可以作为栈顶指针。
链栈
利用链表实现栈,注意链表中指针的方向是从栈顶到栈底。
#includeLinkList.h
template
classstack{
private:
ListstackList;
public:
//constructor
stack(void);
//accessmethod
BOOLstackEmpty(void);
intstackLength(void);
ElemType*getTop(void);
//modificationmethod
voidPush(ElemType*item);
ElemType*Pop(void);
voidclearStack(void);
};
template
voidstack:
:
Push(ElemType*item)
{
stackList.insFirst(item);
}
template
ElemType*stack:
:
Pop(void)
{
ifstackList.listEmpty()returnNULL;
elsereturnstackList.delFirst()
}
3.4队列的类型定义
ADTQueue{
数据对象:
D={ai|ai∈ElemSet,i=1,2,...,n,n≥0}
数据关系:
R1={|ai-1,ai∈D,i=2,...,n}
约定其中a1端为队列头,
an端为队列尾。
基本操作:
InitQueue(&Q)
操作结果:
构造一个空队列Q。
DestroyQueue(&Q)
初始条件:
队列Q已存在。
操作结果:
队列Q被销毁,不再存在。
ClearQueue(&Q)
初始条件:
队列Q已存在。
操作结果:
将Q清为空队列。
QueueEmpty(Q)
初始条件:
队列Q已存在。
操作结果:
若Q为空队列,则返回TRUE,
否则返回FALSE。
QueueLength(Q)
初始条件:
队列Q已存在。
操作结果:
返回Q的元素个数,即队列的长度。
GetHead(Q,&e)
初始条件:
Q为非空队列。
操作结果:
用e返回Q的队头元素。
EnQueue(&Q,e)
初始条件:
队列Q已存在。
操作结果:
插入元素e为Q的新的队尾元素。
DeQueue(&Q,&e)
初始条件:
Q为非空队列。
操作结果:
删除Q的队头元素,并用e返回其值。
}ADTQueue
3.5队列类型的实现
链队列——链式映象
#includeLinkList.h
template
classQueue{
private:
ListqueueList;
public:
//constructor
Queue(void):
queueList();
~Queue(void)
//accessmethod
BOOLqueueEmpty(void)
{returnqueueList.listEmpty()}
intqueueLength(void)
{returnqueueList.listSize()}
ElemType*getHead(void);
//modificationmethod
voidenQueue(ElemType*pitem);
ElemType*deQueue(void);
voidclearQueue(void);
};
template
voidQueue:
:
enQueue(ElemType*pitem)
{
queueList.addTail(pitem);
}
template
ElemType*Queue:
:
deQueue(void)
{
ifqueueList.listEmpty()returnNULL;
elsereturnqueueList.delFirst()
}
循环队列——顺序映象
constintMaxQSize=100;
template
classQueue{
private:
intfront,rear,curSize,maxSize;
ElemType*qList;
public:
//constructor
Queue(void);
~Queue(void)
//accessmethod
BOOLqueueEmpty(void);
BOOLqueueFULL(void);
intqueueLength(void);
ElemType*getHead(void);
//modificationmethod
voidenQueue(ElemType*item);
ElemT