1.定义
栈(stack)是限定仅在表尾进行插入和删除操作的线性表。
根据定义我们知道栈在本质上也是一种线性表,只是在插入和删除操作上进行了限制。
我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构。
栈的插入操作,叫做进栈,也称压栈,入栈,类似子弹入弹夹。
栈的删除操作,叫做出栈,也有的叫作弹栈,如同弹夹中的子弹出夹。
应用:浏览器的“后退”键,Word、Photoshop等文档或图像编辑软件中的撤销(undo)操作等。
2.栈的抽象数据类型
1.InitStack(*S):初始化操作,建立一个空栈S。
2.DestroyStack(*S):若栈存在,则销毁它。
3.ClearStack(*S):将栈清空。
4.StackEmpty(S):若栈为空,返回true,否则返回false。
5.GetTop(S, *e):若栈存在且非空,用e返回S的栈顶元素。
6.Push(*S, e):若栈S存在,插入新元素e到栈S中并成为栈顶元素。
7.Pop(*S, *e):删除栈S中栈顶元素,并用e返回其值。
8.StackLength(S):返回栈S的元素个数。
3.栈的存储结构
(1)栈的顺序存储结构
下面来看一下栈的顺序存储结构的代码实现
typedef int SElemType; //SElemType类型根据实际情况而定,这里假设为int
typedef struct{
SElemType data[MAXSIZE];
int top; //用于栈顶指针
}SqStack;
进栈操作
Status Push(SqStack *s, SElemType e){ //插入元素e为新的栈顶元素
if(S->top == MAXSIZE-1) //栈满
return ERROR;
S->top++; //栈顶指针增加一
S->data[S->top] = e; //将新插入元素赋值给栈顶空间
return OK;
}
出栈操作
Status Pop(SqStack *S, SElemType *e){ //若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR
if(S->top == -1)
return ERROR;
*e = S->data[S->top]; //将要删除的栈顶元素赋值给e
S->top--; //栈顶指针减一
return OK;
}
两栈共享空间
因为栈只准栈顶进出元素,所以不存在线性表插入和删除时需要移动元素的问题。不过它有一个很大的缺陷,就是必须实现确定数组存储空间大小,万一不够用了就需要编程手段来扩展数组的容量,非常麻烦。对于一个栈,我们也只能尽量考虑周全,设计处合适大小的数组来处理,但对于两个相同类型的栈,我们却可以做到最大限度地利用其实现开辟的存储空间来进行操作。
我们都知道使用过程中我们给每个栈分配足够的空间是不太现实的,使用栈的时候,我们也不能保证栈有足够的空间,另外,栈是一个动态的存储结构,各个栈的实际大小在使用的过程中都会发生变化的,有时候其中一个已经上溢了,而另外一个还没用怎么使用。这样必定会造成空间的利用率降低。遇到这种情况我们该怎么办呢?
这时我们引入两站共享空间的概念,我们完全可以用一个数组来存储两个栈,只不过需要点小技巧。
数组有两个端点,两个栈有两个栈底,让一个栈的栈底为数组的始端,即下标为0处,另一个栈为数组的末端,即下标为数组长度n-1处。这样,两个栈如果增加元素,就是两端点向中间延伸。
栈满条件:top1 + 1 == top2
下面是两栈共享空间结构的实现代码
typedef struct{ //两栈共享空间结构
SElemType data[MAXSIZE];
int top1; //栈1栈顶指针
int top2; //栈2栈顶指针
}SqDoubleStack;
入栈操作
对于两栈共享空间的push方法,我们除了要插入元素值参数外,还需要有一个判断是栈1还是栈2的栈号参数stackNumber。
//插入e为新的栈顶元素
Status Push(SqDoubleStack *S, SElemType e, int stackNumber){
if(S->top1+1 == top2) //栈已满,不能再push新元素了
return ERROR;
if(stackNumber == 1) //栈1有元素进栈
S->data[++S->top1] = e; //若栈1则先top1+1后给数组元素赋值
else if(stackNumber == 2) //栈2有元素进栈
S->data[--S->top2] = e; //若栈2则先top2-1后给数组元素赋值
return OK;
}
出栈操作
//若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK否则返回ERROR
Status Pop(SqDoubleStacck *S, SElemType *e, int stackNumber){
if(stackNumber == 1){
if(S->top1 == -1)
return ERROR; //说明栈1已经是空栈,溢出
*e = S->data[S->top1--]; //将栈1的栈顶元素出栈
}
else if(stackNumber == 2){
if(S->top2 == MAXSIZE)
return ERROR; //说明栈2已经是空栈,溢出
*e = S->data[S->top2++]; //将栈2的栈顶元素出栈
}
return OK;
}
事实上,使用这样的数据结构,通常都是当两个栈的空间需求有相反关系时,也就是一个栈增长时另一个栈在缩短的情况。
(2)栈的链式存储结构
由于单链表有头指针,而栈顶指针也是必须的,那干嘛不让它俩合二为一呢,所以比较好的办法是把栈顶放在单链表的头部。另外,都已经有了栈顶在头部了,单链表中比较常用的头结点也就失去了意义,通常对于链栈来说,是不需要头结点呢。
下面是链栈的实现代码
typedef struct StackNode{
SElemType data;
struct StackNode *next;
}StackNode, *LinkStackPtr;
typedef struct LinkStack{
LinkStackPtr top;
int count;
}LinkStack;
链栈的操作绝大部分都和单链表类似,只是在插入和删除上,特殊一些。
进栈操作
//插入元素e为新的栈顶元素
Status Push(LinkStack *S, SElemType e){
LinkStackPtr s= (LinkStackPtr)malloc(sizeof(StackNode));
s->data = e;
s->next = S->top; //把当前的栈顶元素赋值给新结点的直接后继
S->top = s; //将新的结点s赋值给栈顶指针
S->count++;
return OK;
}
出栈操作
//若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR
Status Pop(LinkStack *S, SElemType *e){
LinkStack p;
if(StackEmpty(*S))
return ERROR;
*e = S->top->data;
p = S->top; //将栈顶结点赋值给p
S->top = S->top->next; //使得栈顶指针下移一位,指向后一结点
free(p); //释放结点p
S->count--;
return OK;
}
顺序栈和链栈的区别:
对于空间性能,顺序栈需要事先确定一个固定的长度,可能会存在内存空间浪费的问题,但它的优势是存取时定位很方便,而链栈则要求每个元素都有指针域,这同时也增加了一些内存开销,但对于栈的长度无限制。
4.栈的作用
栈的引入简化了程序设计的问题,划分了不同关注层次,使得思考范围缩小,更加聚焦于我们要解决的问题核心。反之,像数组等,因为要分散精力去思考数组的下标增减等细节问题,反而掩盖了问题的本质。