栈和队列

  • Post author:
  • Post category:其他


在这篇文章将介绍数据结构中的栈和队列及它们的常见应用。在之前写过的文章中或者看本文的读者应该知道有两种存储方式,一种是顺序存储,一种是链式存储,本文将通过这两种存储来实现栈和队列。






栈的基本概念

定义:栈是仅在表尾进行插入或删除操作的

线性表

,又称为后进先除(Last In First Out)线性表。表尾=栈顶(top),表头=栈底(bottom)。不含元素的空表为空栈。

首先这是一个线性表,就具有线性表的特征,假设我现在有5个乒乓球,依次排列。如下图:

在这里插入图片描述

那么我现在有一个杯子,杯口只允许一个乒乓球进入,如下图,我已经有两个球在里面了。

在这里插入图片描述

那么,现在让a3进入,这个就叫入栈。

在这里插入图片描述

再让a3出去,就叫做出栈。

在这里插入图片描述

把最下面的叫做栈底,可以理解成杯子底,上面的叫做栈顶,可以理解为乒乓球的位置差不多。我们通常把栈顶的值赋值为

-1



栈的基本操作

InitStack(&S):构造空的栈,分配内存空间
DestroyStack(&S):销毁栈

StackEmpty(S):判断栈是否为空

GetTop(S,&x):读栈顶元素,若S非空,可以理解为查找,不过只查找栈顶元素

Push(&S,x):入栈,若栈S未满,即将x加入使之成为新栈顶。理解为插入
Pop(&S,&x):若栈S非空,则弹出栈顶元素,并用x返回。理解为删除

它这里后面三个操作都是对栈顶元素的。



顺序存储

定义:顺序栈——栈的顺序存储结构,利用一组连续的存储单元依次存放自栈底到栈顶的数据的元素。这种栈的大小是固定的,也就是一开始就定义好的数组。下面来看下它的结构。

#define MAXSIZE 1000
typedef struct{
	ElemType data[MAXSIZE];//静态数组存放栈中元素
	int top;//栈顶指针
}

我们可以通过下图简单来看下顺序栈示意图。

在这里插入图片描述



Top指向当前位置



判断栈空

bool StackEmpty(SqStack S){
	if(S.top==-1)
		return true;
	else
		return false;
}

这里top一开始指向的是

-1

,才可以判断栈是否为空。

top指针指向栈顶元素



入栈

bool Push(SqStack &S,ElemType x){
	if(S.top==MAXSIZE-1)
		return false;
	S.top=S.top+1;//指针前移
	S.data[S.top]=x//元素入栈
	return true;
}

上面我们说了top是指向栈顶元素,就跟上面那张图一样,MAXSIZE本来是1000,但是数组下标是从0开始,也就是说到999了,其实就已经满了,如果这里

S.top==MAXSIZE-1

就是这么解释的。后面的栈顶指针加1和赋值就很好理解了。



出栈

bool Pop(SqStack &S,ElemType &x){
	if(S.top==-1)
		return false;
	x=S.data[S.top];
	S.top=S.top-1;
	return true;	
}



读栈顶元素

bool GetTop(SqStack S,ElemType &x){
	if(S.top==-1)
		return false;
	x=S.data[S.top];//元素出栈
	return true;
}



Top指向下一个插入位置



入栈

bool Push(SqStack &S,ElemType x){
	if(S.top==MAXSIZE)
		return false;
	S.data[S.top]=x//元素入栈
	S.top=S.top+1;//指针前移
	return true;
}

这里和上面有所区别,因为Top指向的是下一个位置,所以本来指向999应该指向1000了,这里就直接是

MAXSIZE

。然后,后面应该先赋值,再让它指向下一个。



出栈

bool Pop(SqStack &S,ElemType &x){
	if(S.top==0)
		return false;
	S.top=S.top-1;
	x=S.data[S.top];
	return true;	
}

由于

top

指向的是下一个元素的位置,所以如果

top

在0的话,说明其实0是没有插入的,那么栈就是空的。并且

top

需要先指向当前的才能出栈,所以得先让

top-1



共享栈

定义:若需要用到两个相同类型的栈,可用一个数组

data[0...MAXSIZE-1]

来实现两个栈,称之为共享栈。具体我们可以看下图。

在这里插入图片描述

这里就需要两个栈顶指针,因为有两个栈,一个指向

-1

,一个指向

MAXSIZE

。下面来看一下它的结构体。

typedef struct{
	ElemType data[MAXSIZE];//静态数组存放栈中元素
	int top1;//1号栈顶指针
	int top2;//2号栈顶指针
}ShStack;

下面看一下栈空的条件:

1号栈空:top1==-1
2号栈空:top2==MAXSIZE

栈满条件:

top1==top2-1

从图里看,一号栈是从下面往上面,二号栈是上面往下面,也就是,当一号栈当前的等于二号栈当前-1,那么数组就满了,不太理解的可以手动模拟一下这个过程就理解了。

入栈出栈操作

1号栈:top1++;data[top1]=x;2号栈:top2--;data[top2]=x;1号栈:x=data[top1];top1--;2号栈:x=data[top2];top2++;



链式存储

定义:采用链式存储结构的栈为链栈,采用单链表来实现,且规定栈的所有操作都是

在单链表的表头进行

,有点是不存在栈满溢出的情况。

在这里插入图片描述

链式存储的重点是在表头进行插入,也就是它的操作都在表头进行,这里的链表是没有头结点的,也就是只有头指针,只需要在头指针后面加入就行了。



结构

typedef struct StackNode{
	SElemType data;
	struct StackNode *next;
}StackNode;



入栈

bool Push(LinkStack &s,SElemType){
	StackNode *p;
	p=(StackNode *)malloc(sizeof(StackNode));
	p->data=e;
	p->next=s;
	s=p;
	return true;
}

插入以后,要让头指针指向插入的结点。



出栈

bool Pop(LinkStack &s,SElemType &e){
	StackNode *p;
	if(s==NULL)//栈为空
		return false;
	e=s->data;
	p=s;
	s=s->next;
	free(p);
	return true;
}

出栈和入栈不同,链栈中可以一直入栈(理想情况下,不去考虑内存)。而出栈则需要判断栈中是否有东西。



取栈顶元素

SElemType GetTop(LinkStack s){
	if(s!=NULL)
		return s->data;
}



栈总结

栈的内容大概就是这么多了,当然还有一个公式要记,就是如果n个不同的元素入栈,那么出栈就是卡特兰数:
1/(n+1)*Cn2n



队列



队列的基本概念

定义:队列是操作受限的线性表,仅允许在表的一端插入,另一端删除,具有点进先出的特点。可以想象成一车道,只允许一部车通过,第二部车只能在第一部车过这个路口后才能再过。也可以理解成食堂排队。



队列的基本操作

InitQueue(&q):构造空的队列
DestroyQueue(&q):销毁队列

QueueEmpty(q):判断队列是否为空

enQueue(&q,e):去队列,将元素e进队作为队尾元素。
Pop(&q,&e):出队列,从队列中出队一个元素,并将其赋给e。



顺序存储

定义:顺序队列——队列的顺序存储结构,利用一组地址连续的存储单元依次存放队列的数据元素。从下面三张图来理解下空队,入队,和出队。

在这里插入图片描述

空队:队列里什么东西都没有,这时

front==rear



front

指向队头,

rear

指向队尾的下一个元素。当它们相等时,队列为空。

在这里插入图片描述

这里我们可以看到,进行了依次入队操作,front还是指向原来的,rear因为是指向队尾的下一个,所以它向后移动了一个。

在这里插入图片描述

这时进行了出队操作,front就向后移动了,这时候front又等于rear。说明队列为空。

下面来看下结构体。

typedef struct{
	QElemType data[MAXSIZE];//静态数组存放队列中元素
	int front,rear;//队首和队尾指针
}SqQueue;

来看一下几个操作

队空:q.front==q.rear
队满:q.rear==MaxSize
入队:data[rear]=e;rear++
出队:e=data[front];front++

下面是几个具体的操作代码实现



判断队空

bool QueueEmpty(SqQueue q){
	if(q.front==q.rear)
		return true;
	else
		return false;
}



入队

bool enQueue(SqQueue &q,QElemType e){
	if(q.rear==MAXSIZE)
		return false;
	q.data[q.rear]=e;
	q.rear++;
	return true;
}

这里我们判断队满的条件仅仅是q.rear到MAXSIZE的位置,并没有考虑到如果front也在后面,那front前面就会有几个空的,队列就不是满的,没有利用好空间,后面会引入循环队列来解决这个问题。



出队

bool deQueue(SqQueue &q,QElemType &e){
	if(q.front==q.rear)
		return false;
	e=q.data[q.front];
	q.front++;
	return true;
}

这里判断

q.front==q.rear

是为了说明队列中有东西才可以出队。



循环队列

上面提到了为了更好的使用空间,那么现在就引入了循环队列,它是将顺序队列在想法上构成了一个环装,也就是我最后一个入队之后,如果第一个位置是空的,那就可以继续入队。

下面来看下队空和队满的条件

q->front==q->rear//队空
(q->rear+1%MAXSIZE == q->front;//队满

这里队空还是一样,只要它们两个指的是同一个位置就是队空。队满的话举个例子,比如数组大小为4,那么它最大的MAXSIZE应该为4,这时候我数组满了,

(rear+1)%MAXSIZE

其实指向的是

front

的位置。通过下面这张图可以比较好的理解。这里的

rear

是指向队尾元素,而不是队尾元素的下一个。不同时候的

rear

要区分清楚。

在这里插入图片描述



循环队列之判断队空

bool QueueEmpty(SqQueue q){
	if(q.front==q.rear)
		return true;
	else
		return false;
}



循环队列之入队

bool enQueue(SqQueue &q,QElemType e){
	if((q.rear+1)%MAXSIZE==q.front)
		return false;
	q.data[q.rear]=e;
	q.rear=(q.rear+1)%MAXSIZE;
	return true;
}

这里值得注意的是,入队的时候不是

q.rear=q.rear+1

,而是要

%MAXSIZE

,要考虑循环队列的条件。



循环队列之出队

bool deQueue(SqQueue &q,QElemType &e){
	if(q.front==q.rear)
		return false;
	e=q.data[q.front];
	q.front=(q.front+1)%MAXSIZE;
	return true;
}

同样,这里的

q.front

也要

%MAXSIZE

才行。



链式存储

定义:采用链式存储结构的队列为链队列,采用单链表来实现,在单链表表头进行删除/出队操作,在单链表表尾进行插入/入队操作。

在这里插入图片描述

可以从上面这张图来理解,所以我们需要两个指针来指向队头和队尾。下面是它的结构体

typedef struct qNode{
	QElemType data;
	struct qNode *next;
}DataNode;

typedef struct{
	DataNode *front,*rear;
}LinkQuNode;



链队入队操作

入队就是插入队尾

void enQueue(LinkQuNode &q,QElemType e){
	DataNode *p;
	p=(DataNode *)malloc(sizeof(DataNode));
	p->data = e;
	p->next = NULL;
	if(q->rear==NULL)
		q->front=p;
		q->rear=p;
	else{
		q->rear->next=p;
		q->rear=p
	}
}

首先先声明一个p结点,然后给p分配空间,因为插入的结点是最后一个结点,所以得让

p->next



NULL

。q是队列,如果队列的

rear



NULL

,也就是队列中没有其他东西,那么让队列的

front



rear

都指向p。如果队列中有元素,那么就让这个元素的

next

指向p,并且让

p

成为这个队列的队尾,也就是

q->rear=p



链队出队操作

bool deQueue(LinkQuNode &q,QElemType &e){
	DataNode *t;
	if(q->rear==NULL) return false;//队列中没有数据
	t=q->front;
	if(q->front==q->rear)//这样说明只有一个元素
		q->front=q->rear=NULL;
	else
		q->front=q->front->next;//有两个元素就让front指向它后面一个
	e=t->data;
	free(t);
	return true;
}

这里基本的操作在代码注释里也有详细说明了,就不再讲述了。



双端队列

定义:两端都可以进行进队和出队操作的。

双端队列不同于本来的队列,它的选择比较多,以下三种是常见的双端队列。

1.双端队列

这种队列两边都可以输入和输出,如下图:

在这里插入图片描述

2.输出受限的双端队列

这种队列只允许一端输出,但是两端都可以输入,如下图:

在这里插入图片描述

3.输入受限的双端队列

这种队列只允许一端输入,但是两端都可以输出,如下图:

在这里插入图片描述



总结

在本篇文章中,主要讲了栈和队列,基本涉及到了所有的知识点,栈和队列的应用将单独开一篇文章进行解析,毕竟我觉得一篇文章太长不太好,主要是写着累emmm。好了如果文章中有说明问题或者哪里没有表述的很清晰,欢迎在评论区下面给我留言,一起探讨才有进步。



版权声明:本文为weixin_42668972原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。