3.1.1 栈的定义
栈是一种只能在一端进行插入或删除操作的线性表。
栈只能选取同一个端点进行插入和删除操作。
栈的几个概念
- 允许进行插入、删除操作的一端称为栈顶。
- 表的另一端称为栈底。
- 当栈中没有数据元素时,称为空栈。
- 栈的插入操作通常称为进栈或入栈。
- 栈的删除操作通常称为退栈或出栈。
栈的主要特点是“后进先出”,即后进栈的元素先出栈。栈也称为后进先出表。
示例:
设一个栈的输入序列为a,b,c,d,则借助一个栈所得到的输出序列不可能是( )。 A. c,d,b,a B. d,c,b,a C. a,c,d,b D. d,a,b,c
答案为D
【例3.3】一个栈的入栈序列为1,2,3,…,n ,其出栈序列是p1,p2,p3,…,pn。若p1=3,则p2可能取值的个数是多少?
A.n-3 B.n-2 C.n-1 D. 无法确定
答案为B
栈抽象数据类型=逻辑结构+基本运算(运算描述)
栈的几种基本运算如下:
InitStack(&s):初始化栈。构造一个空栈s。
DestroyStack(&s):销毁栈。释放栈s占用的存储空间。
StackEmpty(s):判断栈是否为空:若栈s为空,则返回真;否则返回假。
Push(&S,e):进栈。将元素e插入到栈s中作为栈顶元素。
Pop(&s,&e):出栈。从栈s中退出栈顶元素,并将其值赋给e。
GetTop(s,&e):取栈顶元素。返回当前的栈顶元素,并将其值赋给e。
3.1.2 栈的顺序存储结构及其基本运算实现
栈中元素逻辑关系与线性表的相同,栈可以采用与线性表相同的存储结构。
假设栈的元素个数最大不超过正整数MaxSize,所有的元素都具有同一数据类型ElemType,则可用下列方式来声明顺序栈类型SqStack:
typedef struct
{ ElemType data[MaxSize];
int top; //栈顶指针
} SqStack;
(1)初始化栈InitStack(&s) 建立一个新的空栈s,实际上是将栈顶指针指向-1即可。
void InitStack(SqStack *&s)
{ s=(SqStack *)malloc(sizeof(SqStack));
s->top=-1;
}
(2)销毁栈DestroyStack(&s) 释放栈s占用的存储空间。
void DestroyStack(SqStack *&s)
{
free(s);
}
(3)判断栈是否为空StackEmpty(s) 栈S为空的条件是s->top==-1。
bool StackEmpty(SqStack *s)
{
return(s->top==-1);
}
(4)进栈Push(&s,e) 在栈不满的条件下,先将栈指针增1,然后在该位置上插入元素e
bool Push(SqStack *&s,ElemType e)
{ if (s->top==MaxSize-1) //栈满的情况,即栈上溢出
return false;
s->top++; //栈顶指针增1
s->data[s->top]=e; //元素e放在栈顶指针处
return true;
}
(5)出栈Pop(&s,&e) 在栈不为空的条件下,先将栈顶元素赋给e,然后将栈指针减1。
bool Pop(SqStack *&s,ElemType &e)
{ if (s->top==-1) //栈为空的情况,即栈下溢出
return false;
e=s->data[s->top]; //取栈顶指针元素的元素
s->top--; //栈顶指针减1
return true;
}
(6)取栈顶元素GetTop(s,&e) 在栈不为空的条件下,将栈顶元素赋给e。
bool GetTop(SqStack *s,ElemType &e)
{ if (s->top==-1) //栈为空的情况,即栈下溢出
return false;
e=s->data[s->top]; //取栈顶指针元素的元素
return true;
}
·顺序栈的应用算法设计
【例3.4】 设计一个算法利用顺序栈判断一个字符串是否是对称串。所谓对称串是指从左向右读和从右向左读的序列相同。
bool symmetry(ElemType str[])
{ int i; ElemType e;
SqStack *st;
InitStack(st); //初始化栈
for (i=0;str[i]!='\0';i++) //将串所有元素进栈
Push(st,str[i]); //元素进栈
for (i=0;str[i]!='\0';i++)
{ Pop(st,e); //退栈元素e
if (str[i]!=e) //若e与当前串元素不同则不是对称串
{ DestroyStack(st); //销毁栈
return false;
}
}
DestroyStack(st); //销毁栈
return true;
}
设计一个算法,利用栈将一个正十进制整数转换为二进制数并输出。
#include "sqstack1.cpp" //包含整数栈的基本运算函数
void trans(int n)
{
int e;
SqStack *st;
InitStack(st);
while (n>0)
{ Push(st,n%2);
n=n/2;
}
while (!StackEmpty(st)) //输出对应的二进制数
{ Pop(st,e);
printf("%d",e);
}
printf("\n");
DestroyStack(st);
}
示例
假设输入序列是1、2、…、n。设计一个算法判断通过一个栈能否得到由a[0..n-1](为1、2、…、n的某个排列)指定出栈序列。
例如,n=3,
a[]={2,3,1}是合法出栈序列
a[]={3,1,2}不是合法出栈序列
bool Validseq(int a[],int n)
{ int i,e,k=0; //k扫描a的元素
bool flag;
SqStack *st;
InitStack(st);
for(i=1;i<=n;i++) //处理输入序列1、2、…、n
{ Push(st,i); //i进栈
while (!StackEmpty(st)) //栈不空循环
{ GetTop(st,e); //取栈顶元素e
if (a[k]==e) //匹配的情况
{ Pop(st,e);
k++;
}
else break; //不匹配时退出while循环
}
}
flag=StackEmpty(st);
DestroyStack(st);
return flag;
}
共享栈
若需要用到两个相同类型的栈,可用一个数组data[0..MaxSize-1]来实现这两个栈,这称为共享栈。
共享栈类型:
typedef struct
{ ElemType data[MaxSize]; //存放共享栈中元素
int top1,top2; //两个栈的栈顶指针
} DStack;
共享栈的4要素:
- 栈空条件,栈1空:top1==-1;栈2空:top2==MaxSize;
- 栈满条件:top1==top2-1;
- 元素x进栈操作,进栈1操作:top1++;data[top1]=x;进栈2操作:top2--;data[top2]=x;
- 出栈x操作,出栈1操作,x=data[top1];top1--;出栈2操作,x=data[top2];top2++;
3.1.3 栈的链式存储结构及其基本运算的实现
采用链表存储的栈称为链栈,这里采用带头结点的单链表实现。
链栈的4要素:
- 栈空条件:s->next=NULL
- 栈满条件:不考虑
- 进栈e操作:将存放e的结点插入到头结点之后
- 退栈操作:取出头结点之后结点的元素并删除之
链栈中数据结点的类型LinkStNode声明如下:
typedef struct linknode
{ ElemType data; //数据域
struct linknode *next; //指针域
} LinkStNode;
在链栈中,栈的基本运算算法如下:
(1)初始化栈initStack(&s) 建立一个空栈s。实际上是创建链栈的头结点,并将其next域置为NULL。
void InitStack(LinkStNode *&s)
{ s=(LinkStNode *)malloc(sizeof(LinkStNode));
s->next=NULL;
}
(2)销毁栈DestroyStack(&s) 释放栈s占用的全部存储空间。
void DestroyStack(LinkStNode *&s)
{ LinkStNode *p=s,*q=s->next;
while (q!=NULL)
{ free(p);
p=q;
q=p->next;
}
free(p); //此时p指向尾结点,释放其空间
}
(3)判断栈是否为空StackEmpty(s) 栈S为空的条件是s->next==NULL,即单链表中没有数据结点。
bool StackEmpty(LinkStNode *s)
{
return(s->next==NULL);
}
(4)进栈Push(&s,e) 将新数据结点插入到头结点之后。
void Push(LinkStNode *&s,ElemType e)
{ LinkStNode *p;
p=(LinkStNode *)malloc(sizeof(LinkStNode));
p->data=e; //新建元素e对应的结点p
p->next=s->next; //插入p结点作为开始结点
s->next=p;
}
(5)出栈Pop(&s,&e) 栈非空时,将头结点后继数据结点的数据域赋给e,然后将其删除。
bool Pop(LinkStNode *&s,ElemType &e)
{ LinkStNode *p;
if (s->next==NULL) //栈空的情况
return false;
p=s->next; //p指向开始结点
e=p->data;
s->next=p->next; //删除p结点
free(p); //释放p结点
return true;
}
(6)取栈顶元素GetTop(s,e) 在栈不为空的条件下,将头结点后继数据结点的数据域赋给e。
bool GetTop(LinkStNode *s,ElemType &e)
{ if (s->next==NULL) //栈空的情况
return false;
e=s->next->data;
return true;
}
【例3.5】编写一个算法判断输入的表达式中括号是否配对(假设只含有左、右圆括号)。
算法设计思路
一个表达式中的左右括号是按最近位置配对的。所以利用一个栈来进行求解。这里采用链栈。
bool Match(char exp[],int n)
{ int i=0; char e;
bool match=true;
LinkStNode *st;
InitStack(st); //初始化栈
while (i<n && match) //扫描exp中所有字符
{
if (exp[i]=='(')
Push(st,exp[i]);
else if (exp[i]==')') //当前字符为右括号
{ if (GetTop(st,e)==true)
{ if (e!='(') //栈顶元素不为'('时不匹配
match=false;
else
Pop(st,e); //将栈顶元素出栈
}
else match=false; //无法取栈顶元素时不匹配
}
i++; //继续处理其他字符
}
if (!StackEmpty(st))
match=false;
DestroyStack(st); //销毁栈
return match;
}
注意:只有在表达式扫描完毕且栈空时返回true。
3.1.4 栈的应用
栈和队列都是存放多个数据的容器。通常用于存放临时数据:
如果后放入的数据先处理,则使用栈。
如果先放入的数据先处理,则使用队列。
1. 简单表达式求值
问题描述 :
这里限定的简单表达式求值问题是:用户输入一个包含“+”、“-”、“*”、“/”、正整数和圆括号的合法算术表达式,计算该表达式的运算结果。简单表达式采用字符数组exp表示,其中只含有“+”、“-”、“*”、“/”、正整数和圆括号。 为了方便,假设是合法的算术表达式,例如,exp=“1+2*(4+12)”; 在设计相关算法中用到栈,这里采用顺序栈存储结构。
运算符位于两个操作数中间的表达式称为中缀表达式。例如,"1+2*3"就是一个中缀表达式。
中缀表达式的运算规则:“先乘除,后加减,从左到右计算,先括号内,后括号外”。 因此,中缀表达式不仅要依赖运算符优先级,而且还要处理括号。
算术表达式的另一种形式是后缀表达式或逆波兰表达式,就是在算术表达式中,运算符在操作数的后面。 如"1+2*3"的后缀表达式为"1 2 3 * +"。
后缀表达式特点:
- 没有括号,已考虑了运算符的优先级。
- 只有操作数和运算符,而且越放在前面的运算符来越优先执行。
中缀表达式的求值过程:
- 将中缀算术表达式转换成后缀表达式。
- 对该后缀表达式求值。
while (从exp读取字符ch,ch!='\0')
{ ch为数字:将后续的所有数字均依次存放到postexp中,
并以字符'#'标志数值串结束;
ch为左括号'(':将此括号进栈到Optr中;
ch为右括号')':将Optr中出栈时遇到的第一个左括号'('以前的运算符依次出
栈并存放到postexp中,然后将左括号'('出栈;
ch为其他运算符:
if (栈空或者栈顶运算符为'(') 直接将ch进栈;
else if (ch的优先级高于栈顶运算符的优先级)
直接将ch进栈;
else
依次出栈并存入到postexp中,直到栈顶运算符优先级小于ch的
优先级,然后将ch进栈;
}
若exp扫描完毕,则将Optr中所有运算符依次出栈并存放到postexp中。
针对简单表达式exp:
while (从exp读取字符ch,ch!='\0')
{ ch为数字:将后续的所有数字均依次存放到postexp中,
并以字符'#'标志数值串结束;
ch为左括号'(':将此括号进栈到Optr中;
ch为右括号')':将Optr中出栈时遇到的第一个左括号'('以前的运算符
依次出栈并存放到postexp中,然后将左括号'('出栈;
ch为'+'或'-':出栈运算符并存放到postexp中,直到栈空或者栈顶为'(',
然后将'+'或'-'进栈;
ch为'*'或'/':出栈运算符并存放到postexp中,直到栈空或者栈顶
为'('、'+'或'-',然后将'+'或'-'进栈;
}
若exp扫描完毕,则将Optr中所有运算符依次出栈并存放到postexp中。
算法:将算术表达式exp转换成后缀表达式postexp。
void trans(char *exp,char postexp[])
{ char e;
SqStack *Optr; //定义运算符栈指针
InitStack(Optr); //初始化运算符栈
int i=0; //i作为postexp的下标
while (*exp!='\0') //exp表达式未扫描完时循环
{ switch(*exp)
{
case '(': //判定为左括号
Push(Optr,'('); //左括号进栈
exp++; //继续扫描其他字符
break;
case ')': //判定为右括号
Pop(Optr,e); //出栈元素e
while (e!='(') //不为'('时循环
{ postexp[i++]=e; //将e存放到postexp中
Pop(Optr,e); //继续出栈元素e
}
exp++; //继续扫描其他字符
break;
case '+': //判定为加或减号
case '-':
while (!StackEmpty(Optr)) //栈不空循环
{ GetTop(Optr,e); //取栈顶元素e
if (e!='(') //e不是'('
{ postexp[i++]=e; //将e存放到postexp中
Pop(Optr,e); //出栈元素e
}
else //e是'(时退出循环
break;
}
Push(Optr,*exp); //将'+'或'-'进栈
exp++; //继续扫描其他字符
break;
case '*': //判定为'*'或'/'号
case ‘/’:
while (!StackEmpty(Optr)) //栈不空循环
{ GetTop(Optr,e); //取栈顶元素e
if (e=='*' || e=='/')
{ postexp[i++]=e; //将e存放到postexp中
Pop(Optr,e); //出栈元素e
}
else //e为非‘*’或‘/’时退出循环
break;
}
Push(Optr,*exp); //将'*'或'/'进栈
exp++; //继续扫描其他字符
break;
default: //处理数字字符
while (*exp>='0' && *exp<='9') //判定为数字字符
{ postexp[i++]=*exp;
exp++;
}
postexp[i++]='#'; //用#标识一个数值串结束
}
}
while (!StackEmpty(Optr)) //此时exp扫描完毕,栈不空时循环
{ Pop(Optr,e); //出栈元素e
postexp[i++]=e; //将e存放到postexp中
}
postexp[i]='\0'; //给postexp表达式添加结束标识
DestroyStack(Optr); //销毁栈
}
while (从postexp读取字符ch,ch!='\0')
{ ch为'+':从Opnd栈中出栈两个数值a和b,计算c=b+a;将c进栈;
ch为'-':从Opnd栈中出栈两个数值a和b,计算c=b-a;将c进栈;
ch为'*':从Opnd栈中出栈两个数值a和b,计算c=b*a;将c进栈;
ch为'/':从Opnd栈中出栈两个数值a和b,若a不零,计算c=b/a;将c进栈;
ch为数字字符:将连续的数字串转换成数值d,将d进栈;
}
返回Opnd栈的栈顶操作数即后缀表达式的值;
算法:计算后缀表达式postexp的值。
double compvalue(char *postexp)
{ double d, a, b, c, e;
SqStack1 *Opnd; //定义操作数栈
InitStack1(Opnd); //初始化操作数栈
while (*postexp!='\0') //postexp字符串未扫描完时循环
{ switch (*postexp)
{
case '+': //判定为'+'号
Pop1(Opnd,a); //出栈元素a
Pop1(Opnd,b); //出栈元素b
c=b+a; //计算c
Push1(Opnd,c); //将计算结果c进栈
break;
case '-': //判定为'-'号
Pop1(Opnd,a); //出栈元素a
Pop1(Opnd,b); //出栈元素b
c=b-a; //计算c
Push1(Opnd,c); //将计算结果c进栈
break;
case '*': //判定为'*'号
Pop1(Opnd,a); //出栈元素a
Pop1(Opnd,b); //出栈元素b
c=b*a; //计算c
Push1(Opnd,c); //将计算结果c进栈
break;
case '/': //判定为'/'号
Pop1(Opnd,a); //出栈元素a
Pop1(Opnd,b); //出栈元素b
if (a!=0)
{ c=b/a; //计算c
Push1(Opnd,c); //将计算结果c进栈
break;
}
else
{ printf("\n\t除零错误!\n");
exit(0); //异常退出
}
break;
default: //处理数字字符
d=0; //转换成对应的数值存放到d中
while (*postexp>='0' && *postexp<='9')
{ d=10*d+*postexp-'0';
postexp++;
}
Push1(Opnd,d); //将数值d进栈
break;
}
postexp++; //继续处理其他字符
}
GetTop1(Opnd,e); //取栈顶元素e
DestroyStack1(Opnd); //销毁栈
return e; //返回e
}
设计求解程序
建立如下主函数调用上述算法:
int main()
{ char exp[]="(56-20)/(4+2)"; //可将exp改为键盘输入
char postexp[MaxSize];
trans(exp,postexp);
printf("中缀表达式:%s\n",exp);
printf("后缀表达式:%s\n",postexp);
printf("表达式的值:%g\n",compvalue(postexp));
return 0;
}
2、用栈求解迷宫问题
问题描述
给定一个M×N的迷宫图、入口与出口、行走规则。求一条从指定入口到出口的路径。 所求路径必须是简单路径,即路径不重复。
在算法中用到的栈采用顺序栈存储结构,即将栈声明为:
typedef struct
{ int i; //当前方块的行号
int j; //当前方块的列号
int di; //di是下一可走相邻方位的方位号
} Box; //定义方块类型
typedef struct
{ Box data[MaxSize];
int top; //栈顶指针
} StType; //声明顺序栈类型
用栈求一条迷宫路径的算法: (xi,yi)--》 (xe,ye)
bool mgpath(int xi,int yi,int xe,int ye)
{ Box path[MaxSize], e; int i,j,di,i1,j1,k; bool find;
StType *st; //定义栈st
InitStack(st); //初始化栈顶指针
e.i=xi; e.j=yi; e.di=-1; //设置e为入口
Push(st,e); //方块e进栈
mg[xi][yi]=-1; //入口的迷宫值置为-1避免重复
while (!StackEmpty(st)) //栈不空时循环
{ GetTop(st,e); //取栈顶方块e
i=e.i; j=e.j; di=e.di;
if (i==xe && j==ye) //找到了出口,输出该路径
{ printf("一条迷宫路径如下:\n");
k=0;
while (!StackEmpty(st))
{ Pop(st,e); //出栈方块e
path[k++]=e; //将e添加到path数组中
}
while (k>=1)
{ k--;
printf("\t(%d,%d)",path[k].i,path[k].j);
if ((k+2)%5==0) //每输出每5个方块后换一行
printf("\n");
}
printf("\n");
DestroyStack(st); //销毁栈
return true; //输出一条迷宫路径后返回true
}
find=false;
while (di<4 && !find) //找相邻可走方块(i1,j1)
{ di++;
switch(di)
{
case 0:i1=i-1; j1=j; break;
case 1:i1=i; j1=j+1; break;
case 2:i1=i+1; j1=j; break;
case 3:i1=i; j1=j-1; break;
}
if (mg[i1][j1]==0) find=true;
//找到一个相邻可走方块,设置find为真
}
if (find) //找到了一个相邻可走方块(i1,j1)
{ st->data[st->top].di=di; //修改原栈顶元素的di值
e.i=i1; e.j=j1; e.di=-1;
Push(st,e); //相邻可走方块e进栈
mg[i1][j1]=-1;
//(i1,j1)迷宫值置为-1避免重复
}
else //没有路径可走退栈
{ Pop(st,e); //将栈顶方块退栈
mg[e.i][e.j]=0;
//让退栈方块变为1
}
}
DestroyStack(st); //销毁栈
return false; //表示没有可走路径
}
设计求解程序
建立如下主函数调用上述算法:
int main()
{ if (!mgpath(1,1,M,N))
printf("该迷宫问题没有解!");
return 1;
}
本节完.