栈
- 栈的基本概念
- 栈的顺序存储实现
- 栈的链式存储实现
- 队列的基本概念
- 队列的顺序实现
- 队列的链式实现
- 双端队列
- 栈的应用(在括号匹配中的作用)
- 栈的应用(在表达式求值中的应用)
- 栈的应用(在递归中的应用)
- 队列的应用
- 特殊矩阵的压缩存储
【1】
定义:栈(stack)是只允许在一端进行插入或删除操作的线性表
是一种操作受限的线性表。
重要术语:
- 空栈:相当于线性表的空表
- 栈顶:允许插入和删除的这一端
- 栈底:不允许插入和删除的一端
【特点:后进先出——Last In FIrst Out(LIFO)】
与线性表的区别
1:逻辑结构上:与普通线性表相同
2:但是在一些数据的运算上:插入删除操作有区别
栈的基本操作
【创建销毁】【都是O(1)时间复杂度】
InitStack(&S)初始化栈,构造一个空栈S,分配内存空间
DestroyStack(&S)销毁栈,并释放stackS所占的内存空间
【增、删】
push(&s,x)进栈,若栈s没满,栈顶指针先加一【++s.top】,再将x加入送到栈顶使之成为新栈顶
pop(&s,&x)出栈,若栈s非空,则弹出栈顶元素,并用x返回【这一个操作删除了栈顶元素】【先取栈顶元素值,再将栈顶指针减1【S.top–】】
【查】
GetTop(S,&x):读取栈顶元素,若栈s非空,则用x返回栈顶元素【与pop不同的是这一步只读取不删除】
【其他操作】
判空操作:StackEmpty(S):判断一个栈S是否为空,若S为空,则返回true,否则返回false。
判满操作:s.topMaxisize-1;
栈长:S.top+1
栈顶指针:S.top,初始时设置为S.top-1;栈顶元素S.data[s.top]
【2】用顺序存储方式实现的栈
因为是顺序存储,其实和顺序表是一样的
类比得
[2-1]初始化栈
#define MaxSize 10
typedef struct{
ElemType data[maxsize];//静态数组存放栈中元素
int top;//栈顶指针
}Sqstack;
void testStack(){
SqStack S;//声明一个顺序栈(分配空间)
}
//判空操作
bool StackEmpty(sqStack S){
if(s.top==-1)//空
return true;
else//非空
return false;
//初始化栈
void InitStack(&S){
S.top=-1;//初始化栈顶指针
}
其中,顺序存储的方式给各个数据元素分配连续的存储空间,大小为MaxSize*sizeof(ElemType)
此时最大容量为10,那么就开辟十个空间+top元素的字节
而top的作用是指向栈顶元素,如果此时压入栈S4个元素,那么top此时=4.
但是初始时,top指向的是data【0】,而此时data【0】中还没有存放数据,所以这样是不合理的,合理的做法应该在初始时让top=-1。
【判空操作中体现了这一点】
【2-2】入栈操作
#define MaxSize 10
typedef struct{
ElemType data[maxsize];
int top;
}SqStack;
bool Push(SqStack &S,Elemtype x){
if(S.top==maxsize-1)//栈满
return false;
S.top=S.top+1;
S.data[S.top]=x;
//上两句相同效果的代码时是:S.data[++S.top]=x;
//将top指针先+1,例如从-1的位置移到下标为0的地方,再将数据放入其中。
【2-3】出栈操作
#define MaxSize 10
typedef struct{
ElemType data[maxsize];
int top;
}sqStack;
//出栈操作
bool Pop(SqStack &S,ElemType &x){
if(s.top==-1)
return false//栈空报错
x=S.data[s.top];//栈顶元素先出栈,将出栈数据赋值给x
s.top=s.top-1;//栈顶指针下移
return true;//这里的等价代码是x=S.data[s.top--]
}
//只是逻辑删除,其实数据还是存留在内存中
【针对2-2/2-3】【另一种方式】
#define MaxSize 10
typedef struct{
ElemType data[maxsize];
int top;
}sqStack;
//初始化栈
void InitStack(&S){
S.top=0;//初始指向0
}
bool StackEmpty(SqStack S){
if(S.top==0)
return true;
else
return false;
}
void testStack(){
SqStack S;//声明一个顺序栈(分配空间)
InitStack(S);
}
//在这个方式下,如果想要元素入栈时需要先讲元素放入其中再将指针变化,那么就和原先方式相反。
bool Push(SqStack &S,Elemtype x){//入栈
if(S.top==maxsize)//栈满
return false;
S.data[S.top]=x;//元素先入,因为是0对0这样的关系
S.top=S.top+1;//改变下标上移
//等价于S.data[s.top++]=x;
//同理,出栈时
bool Pop(SqStack &S,ElemType &x){
if(s.top==0)
return false//栈空报错
s.top=s.top-1;//栈顶指针下移
x=S.data[s.top];//栈顶元素出栈,将出栈数据赋值给x
return true;//这里的等价代码是x=S.data[--s.top]
}
这个处理方案与第一个不同的点在于对于栈顶指针的区别,影响到后续判空判满的操作。
【共享栈】
1:两个栈共享同一片内存空间,两个栈从两边往中间增长。
2:初始化:0号栈栈顶指针初始时top0=-1;1号栈栈顶指针初始时top1=MaxSize。
3:栈满条件:top0+1==top1
【3】用链式存储方式实现的栈
【如何理解?将链栈看作一个只能执行头插操作的链表,因此链表的头部就作为栈顶,尾部作为栈底】
用头插法建立单链表:进栈操作。
只能对头结点的“后删”操作:出栈操作
因此,可以实现带头结点和不带头结点的操作。
但局限的是:进栈/出栈都只能在栈顶一端进行(链头作为栈顶)
推荐使用不带头结点的实现方式。
将链表头部作为栈顶的一端,可以避免在实现数据 “入栈” 和 “出栈” 操作时做大量遍历链表的耗时操作。
链表的头部作为栈顶,意味着:
1:在实现数据"入栈"操作时,需要将数据从链表的头部插入;
2:在实现数据"出栈"操作时,需要删除链表头部的首元节点;
链栈的实现思路同顺序栈类似,顺序栈是将数顺序表(数组)的一端作为栈底,另一端为栈顶;链栈也如此,通常我们将链表的头部作为栈顶,尾部作为栈底,如图 1 所示:
链栈示意图
typedef struct lineStack{
int data;
struct lineStack * next;
}lineStack;
//stack为当前的链栈,a表示入栈元素
lineStack* push(lineStack * stack,int a){
//创建存储新元素的节点
lineStack * line=(lineStack*)malloc(sizeof(lineStack));
line->data=a;
//新节点与头节点建立逻辑关系
line->next=stack;
//更新头指针的指向
stack=line;
return stack;
}
链栈元素出栈
示意图
//栈顶元素出链栈的实现函数
lineStack * pop(lineStack * stack){
if (stack) {
//声明一个新指针指向栈顶节点
lineStack * p=stack;
//更新头指针
stack=stack->next;
printf("出栈元素:%d ",p->data);
if (stack) {
printf("新栈顶元素:%d\n",stack->data);
}else{
printf("栈已空\n");
}
free(p);
}else{
printf("栈内没有元素");
return stack;
}
return stack;
}