考纲要求 💕
第3章 栈和队列
- 1.
栈
(1)栈的定义及基本运算
(2)栈的存储结构及基本运算的实现
(3)栈的简单应用
- 2.
队列
(1)队列的定义及基本运算
(2)队列的存储结构及基本运算的实现
(3)队列的简单应用
知识点(考纲要求)
-
1.掌握栈和队列的特性以及它们之间的差异。
-
2.重点掌握顺序栈和链栈上实现栈的基本操作,注意栈满和栈空的条件。
-
3.重点掌握顺序队列和链队列上实现队列的基本操作,注意循环队列上队满和队空的条件。
-
4.了解栈和队列的简单应用。
前言:
栈和队列,严格意义上来说,也属于线性表,因为它们也都用于存储逻辑关系为 "一对一" 的数据,但由于它们比较特殊,因此将其单独作为一章,做重点讲解。
使用栈结构存储数据,讲究“先进后出
”,即最先进栈的数据,最后出栈;使用队列存储数据,讲究 "先进先出
",即最先进队列的数据,也最先出队列。
既然栈和队列都属于线性表,根据线性表分为顺序表和链表的特点,栈也可分为顺序栈和链表
,队列也分为顺序队列和链队列
。
下面先讲解栈吧
1. ▶️ 栈 ✨
栈的基本定义
首先我们来讲讲栈,栈是只能在表尾进行插入或删除操作的线性表
,通常我们称表尾端为栈顶
,表头端为栈底
,它是一种先进后出
的线性表,既只能在表尾端插入元素,称为入栈
,也只能在表尾端删除元素,称为出栈
,如下图所示:
栈既然也是线性表,那么它也有顺序存储结构和链式存储结构两种表示方法,这两种表示方法实现类似。在之后我们将会讲解栈的顺序存储结构实现
❗✨ 进出栈的可能顺序
值得注意的是,栈对线性表的插入和删除是在位置上进行了限制,但是并没有对进出时机进行限制。也就说,刚进去的元素也可以立即出栈,只要保证位置是栈顶即可
比如现在有3个元素1 11、2 22、3 33依次进栈,会有哪些出栈次序呢?
- 第一种: 1 11、2 22、3 33进,然后再3 33、2 22、1 11。出栈次序为3 332 221 11
- 第二种:1 11进、1 11出;2 22进、2 22出;3 33进、3 33出。出栈次序为123 123123
- 第三种:1 11进、2 22进、2 22出、1 11出、3 33进、3 33出。出栈次序为213 213213
- 第四种:1 11进、1 11出、2 22进、3 33进、3 33出、2 22出。出栈次序为132 132132
- 第五种:1 11进、2 22进、2 22出、3 33进、3 33出、1 11出。出栈次序为231 231231
列举完毕共有5种情况。除此之外,像312 312312这种情况是绝对不可能出现的。因为3 33先出栈,就意味着3 33曾经进栈,既然3 33都进栈了,那么也就意味着1 11和2 22已经进栈了,此时2 22一定是在1 11的上面,出栈只能是321 321321
公式:
其实,n 个不同元素进栈,出栈元素不同排列的个数可由
-
- 卡特兰数证明有兴趣可以移步点击跳转
栈的基本操作
一个栈的基本操作如下
InitList(&L)
:初始化表DestoryList(&L)
:销毁操作ListInsert(&L,i,e)
:插入操作ListDelete(&L,i,&e)
:删除操作LocateElem(L,e)
:按值查找GetElem(L,i)
:按位查找
其它常用操作
Length(L)
:求表长PrintList(L)
:输出操作Empty(L)
:判空操作
下面先来实现栈的顺序表示把
❗ 1.1 栈的顺序表示 ✨
❗ 1.1.1 结构定义
顺序栈:顺序栈使用数组
来实现。其中下标为0
的一端作为栈底,将数组尾部
作为栈顶,以进行插入和删除
由于尾部元素作为栈顶,也即最后一个元素是栈顶元素,所以需要使用一个 变量top
进行标识,top
之外元素将不属于栈的定义范围,通常把top=-1
定义为空栈
顺序栈的结构定义如下:
#define MaxSize 10 //定义栈中元素最大个数
typedef int ElemType //表元素类型 int
typedef struct
{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top;//栈顶指针
}SqStack;
❗ 1.1.2 初始化栈
就是初始化,刚开始栈为空,条件为 top=-1
void InitStack(SqStack &S)
{
S.top=-1; //栈顶指针
}
//判断一下栈空不
bool StackEmpty(SqStack S)
{
if(S.top==-1) //栈空
return true;
else
return false;//栈不空
}
❗ 1.1.3 进栈
进栈:进栈时,先栈顶指针+1,后进行元素赋值(若top
初值为0逻辑恰好相反)
代码如下:
bool Push(SqStack &S,ElemType e)
{
if(S.top==MaxSize-1)
return false; //栈满了
S.top+=1; //指针先+1
S.date[S.top]=e; //数组top元素e——入栈
return true;
}
❗ 1.1.4 出栈
出栈:出栈时,先保存元素,后栈顶指针-1(若top
初值为0逻辑恰好相反)
- 注意这种删除只是逻辑上的删除,其元素仍然会留在那片区域里。因为我们研究的是栈,只研究的是
0~top
这个范围内的情况
代码如下,注意栈空判断(if(s.top==-1))
bool Pop(SqStack &S,ElemType &e)
{
if(S.top==-1)
return false;//栈空
e=S.data[S.top]; //栈顶元素先出栈
S.top-=1; //指针减1
return true;
}
这就相当于top-1,然后把出去的栈元素保留给e。
注意,这里出栈的top指针的数据并没有消去,还残留在内存中,只是逻辑上删除了。如下图:
❗ 1.1.5 读取栈顶元素
bool GetTop(SqStack& S,ElemType &e)
{
if(S.top==-1)
return false;//栈空
e=S.data[S.top];
return true;
}
顺序栈其缺点就在于事先要确定空间大小,万一不够用了还需要进行扩容,很是麻烦。有一种解决方法就是利用共享栈:二个栈共享同一片空间。
❗ 1.1.6 共享栈
共享栈:有两个相同类型
的栈,如果一个栈空间满了,就可以借助另一个栈的空间。 具体来说,可以让一个栈的栈底为“栈”始端,即数组下标0
处,另一个栈的栈底为“栈”末端,即数组下标n-1
处,增加元素时,两端点相向而行
如下图描述:
最后合成起来是这样的:
不难想象,其判空和判满条件如下
- 判空:
top1==-1
且top2==n
(超出范围) - 判满:普通情况下,
top1
和top2
一般不碰面。但是当top1+1==top2
时肯定就满了
他们的进出栈:
-
当0号栈进栈时top0先加1再赋值,1号栈进栈时top1先减1再赋值;出栈时则刚好相反。
- 共享栈的结构定义如下:
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct
{
ElemType data[MaxSize];
int top1;//栈1的栈顶指针
int top2;//栈2的栈顶指针
}SqDoubleStack;
- 初始化代码如下:
void InitStack(SqDoubleStack &S)
{
S.top1=0;
S.top2=MaxSize;
}
- 共享栈压栈代码如下:
bool Push(SqDoubleStack &S,ElemType e,int StackNumber)
{
if(S.top1+1==S.top2) //栈满
return false;
if(StackNumber==1)//栈1进栈
S.data[++s.top1]=e; //先指针+1,再赋值
if(StackNumber==2)//栈1进栈
S.data[--s.top2]=e; //先指针-1,再赋值
return true;
}
- 共享栈出栈代码如下:
bool Pop(SqDoubleStack& S,ElemType &e,int StackNumber)
{
if(StackNumber==1) //栈1出栈
{
if(S.top1==-1)//栈1已经是空栈
return false;
e=S.data[S.top1--]; //先取值,再指针-1
}
else if(StackNumber==2) //栈2出栈
{
if(S.top2==MaxSize)//栈2是空栈
return false
e=S.data[S.top2++]; //先取值,再指针+1
}
return true;
}
上面完成了共享栈,下面来看看链式表达栈
❗ 1.2 栈的链式表示
1.2.1 链栈的定义 ✨
**链栈
:就是栈的链式存储结构。
❗ 1.2.2 结构定义
typedef struct StackNode
{
ElemType data; //数据域
struct StackNode* next; //指针域
}StackNode;
❗ 1.2.3 初始化链栈
可分为带头结点的和不带的
带头结点
//初始化一个链栈
void initstack(StackNode &L)
{
L=(StackNode*)malloc(sizeof(StackNode));//制造头节点
L->next=NULL;
}
不带头结点
//初始化一个链栈
void initstack(StackNode &L)
{
L->next=NULL;
}
判断栈空
//判断链栈是否为空
int isEmpty(StackNode &L)
{
if(L->next==NULL)
return true;
else
return false;
}
❗ 1.2.3 进栈
进栈:链栈进栈相当于单链表的插入操作
代码如下,无须考虑栈满情况
//进栈
void push(StackNode &L,int x)
{
StackNode *p;
p=(StackNode*)malloc(sizeof(StackNode)); //申请一个新节点
p->next=NULL;
//头插法
p->data=x;
p->next=L->next;
L->next=p;
}
插入了头结点后面
❗ 1.2.4 出栈
出栈:链栈进栈相当于单链表的删除操作
代码如下,- 需要考虑栈空情况
:
//出栈
int pop(StackNode &L,int &x)
{
StackNode *p; //新建一个结点,用于销毁
if(L->next==NULL) //栈空,出不了
return false;
//单链表删除
p=L->next;
x=p->data;
L->next=p->next;
free(p);
return true;
}
我看有些具有的是有二个结构体,一个是结点,一个记录数量和top指针
参考:
2. ▶️ 队列 ✨
✨ 队列基本概念
队列(Queue)
:是一种只允许在一端插入
,在另一端删除的线性表
。我们把允许插入的一端称之为队尾(rear)
,把允许删除的一端称之为队头(front)
。不含任何数据元素的队列称之为空队列
。队列遵循先进先出(FIFO)
原则
队列的基本操作
一个队列的基本操作如下
InitQueue(&Q)
:初始化队列DestoryQueue(&Q)
:销毁队列EnQueue(&Q,x)
:入队DeQueue(&Q,&x)
:出队GetHead(Q,&x)
:读队头元素QueueEmpty(Q)
:队列判空
下面就来实现队列的顺序表达和链式表达和一些操作
❗ 2.1 顺序队列
❗ 2.1.1 结构定义
这里实现队列的顺序结构:
define MaxSize 10 //定义队列中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //存在数组元素
int front,rear; //队头指针和队尾指针
} SqQueue;
然后进行初始化
❗ 2.1.2 初始化顺序队列
void InitQueue{SqQueue &Q){
//初始时候,队头队尾指针指向0
Q.rear=Q.front=0;
}
判空 ✨
bool QueueEmpty(SqQueue Q){
if(Q.rear==Q.front) //队空条件
return true;
else
return false;
}
有助于下面理解入队出队
❗ 2.1.3 入队
- 入队列
入队列的演示如下图:
只能从队尾插入了
bool EnQueue(SqQueue &Q,ElemType x){
if(Q.rear==MaxSize) //队满,看下面解释。其实是假溢出了,本来应该是回到0
return false;
Q.data[Q.rear]=x; //把x插入队尾
Q.rear=Q.rear+1; //队尾指针后移
return true;
}
所以这里看着像是越界了,所以要引入咱们下面要讲的循环队列
参考 : 顺序队列的基本操作C语言详解
❗ 2.1.4 出队
- 出队列
出队列的演示如下图:
bool DeQueue(SqQueue &Q, int &e)
{
if (Q->rear == Q->front) //队空
return false;
e = Q->data[Q->front]; //对头元素赋值给e
Q->front++; //对头指针后移
return true;
}
下面关于针对上面越界的问题,引用循环队列
❗ 2.2 循环队列
2.2.1 ❗ 定义和判空判满
循环队列:为了解决假溢出问题,可以将数组“头尾相接”形成一种逻辑上的环形结构
这种“环形”只是一种逻辑上的感觉,其底层仍然是连续的空间,想要实现操作上的环形那么就必须要对其下标的变换做一定的文章。(其实就是利用了%
元素,例如1%12
会把结果映射到0~11这个范围)
rear
移动时:rear=(rear+1)%Maxsize)
front
移动时:front=(front+1)%Maxsize)
所以使用这种方式,当a5插入时,rear
=(4+1)%5=0,于是就又回到了开头
数组空间毕竟是有限的,那么这样的结构其队空队满的条件是什么呢?如下
- 队空:
rear=front
- 队满:
(rear+1)%Maxsize=front
比如下面,rear
开始为1,插入 a7后,rear
=(1+1)%5=2=front
,此时队满
2.2.2 ❗ 结构定义
typedef struct
{
DataType data[MaxSize];
int front;//头指针
int rear;//尾指针。若队列不空,指向队尾元素的下一个位置
}SqQueue;
2.2.3 ❗ 初始化循环队列
bool InitQueue(SqQueue &Q)
{
Q.front=0; //开始都是队头 0
Q.rear=0;
return true;
}
判空 ✨
bool QueueEmpty(SqQueue Q){
if(Q.rear==Q.front) //队空条件
return true;
else
return false;
}
2.2.4 ❗ 入队
入队:入队时,先元素赋值,后rear
指针+1
(因为要保证rear
指向队尾下一个位置)
bool EnQueue(SqQueue &Q,DataType e)
{
if(Q.front==(Q.rear+1)%MaxSize)
return false;//队满
Q.data[Q.rear]=e;
Q.rear=(Q.rear+1)%MaxSize;
return true;
}
2.2.5 ❗ 出队
bool DeQueue(SqQueue& Q,DataType *e)
{
if(Q.front==Q.rear)
return false;//队空
*e=Q.data[Q.front];
Q.front=(Q.front+1)%MaxSize;
return true;
}
所以说基本上还是用循环队列牢靠点,记住改变的是(Q.front+1)%MaxSize
,指针这样+1,然后是队满是Q.front==(Q.rear+1)%MaxSize
。
参考: 循环队列基本操作C语言详解
下面要讲解的就是链式队列了
❗ 2.3 链式队列
参考: 链式队列的基本操作C语言详解
❗ 2.3.1 定义
链式队列:其本质仍然是单链表,不过只能尾进头出
。为了操作方便,将front
指针指向头结点
,将rear
指针指向队尾结点
于是,在队列为空时,front
和rear
都将指向头结点
❗ 2.3.2 结构定义
typedef struct QNode//结点
{
DateType data;
struct QNode* next;
}QNode;
typedef struct LinkQueue//链式栈
{
QNode* rear;//队尾指针
QNode* front;//队头指针
}LinkQueue;
❗ 2.3.3 初始化链式队列
分带头结点和不带头结点,一般都是带头结点的
带头结点
void InitQueue(LinkQueue &Q)
{
//初始时候,front和rear都指向头结点
Q.front=Q.rear=(QNode *)malloc(sizeof(QNode));//申请结点空间
Q.front->next=Q.rear->next=NULL;
}
不带头结点
void InitQueue(LinkQueue &Q)
{
//初始时候,front和rear是指针在队头,是空队
Q.front=Q.rear=NULL;
}
❗ 2.3.4 判断队空
bool IsEmpty(LinkQueue Q)
{
if (Q.front == Q.rear) //头尾指针同时指向头结点为空
return true;
else return false;
}
❗ 2.3.5 入队
带头结点
void EnQueue(LinkQueue &Q, int e)
{
QNode *s=(QNode *)malloc(sizeof(QNode));//申请结点空间
if (s == NULL)
return false; //申请内存空间失败
s->data = e;
s->next = NULL;
Q->rear->next = s; //新结点插入rear之后
Q->rear = s; //rear指针指向这个新建立的队尾结点
}
不带头结点
不带头结点的队列,第一个元素入队时候需要特殊处理,使新建立的指针指向front和rear
void EnQueue(LinkQueue &Q, int e)
{
QNode *s=(QNode *)malloc(sizeof(QNode));//申请结点空间
if (s == NULL)
return false; //申请内存空间失败
s->data = e;
s->next = NULL;
/* 第一个元素入队*/
if(Q.front==NULL){
Q.front=s; //修改队头队尾指针
Q.rear=s;}
else
{
Q.rear->next=s; //新结点插入到rear结点之后
Q.rear=s; //rear指针指向这个新建立的队尾结点
}
}
❗ 2.3.6 出队
带头结点
bool DeQueue(LinkQueue &Q, int &e)
{
if (Q->front == Q->rear) //队空
return false;
QNode *p= Q->front->next; //将欲删除的结点暂存给p结点
e = p->data; //获取元素值
Q->front->next = p->next; //修改头结点的next指针,跳过要删除的
if (Q->rear == p) //删除的是最后一个结点
Q->rear = Q->front; //修改rear指针,使只剩头结点
free(p);
return true;
}
不带头结点
bool DeQueue(LinkQueue &Q, int &e)
{
if (Q->front == NULL) //队空
return false;
QNode *p= Q->front; //p指向此次出队的结点
e = p->data; //获取队头元素
Q->front=p->next; //修改front指针,跳到下一个,等于出去一个了
if (Q->rear == p) //删除的是最后一个结点
Q->rear=Q->front=NULL; //front和rear都指向NULL,空队
free(p);
return true;
}
❗ 2.4 双端队列
前言:
- 双端队列考点主要在输出合法次序的判断上
- 双端队列在C++是作为了STL容器中
deque
的底层结构,有兴趣可以了解:6-5-2:STL之stack和queue——双端队列deque
定义
如下几种情况:
❗ 2.4.1 双端队列输出序列合法性判断
双端队列是一种非常特殊的线性表,如果将其限制在只能在一端插入、删除那么它就退化为了栈结构,在这种基础上又有两个变化方向
- 如果继续限制另一端只能删除那么就是输入受限的双端队列
- 如果继续限制另一端只能插入那么就是输出受限的双端队列
下面举个例子说明:
测试用例:
- 输入数据的顺序为 1,2,3,4
先不管输出是否合法,总的输出的可能性总共有:
下面判断他是否合法
- 栈规则 :先进后出
既然栈是一种特殊的双端队列,那么对于栈来说是合法的次序
,自然对双端队列也是合法的次序(输入受限和输出受限肯定都满足) ,根据卡特兰数计算可知:
下面绿色的是合法的:
为啥呢?比如拿一个做例子:
- 2134:是合法的,12进->2出->1出->3进->3出->4进->4出
- 3124:不合法的,123进->3出->1没法先出,绕过2
接着需要判断红色部分的序列,因为红色部分在输入受限和输出受限中可能会不满足
- 输入受限和输出受限
输入受限双端队列合理序列:绿色部分+绿色部分(下划线):
这里下划线是输入受限合理的,举个例子:
- 4132: 只能一个地方输入,但输出二个地方。 1234进->4出->123->1出->23->3出->2出,合法
- 4213:1234进->4出->123->2在中间出不了,不能按照顺序,不合法
输出受限双端队列合理序列:绿色部分+绿色部分(下划线):
这里下划线是输出受限合理的,举个例子:
- 4132: 二个地方输入,但输出一个地方。 删除是一个方向,要成功得输入是2314,3不可能绕过12在中间,无论左边进还是右边进。
- 4213: 从右向左输入12,这时候是12,再从左向右输入3,是312,再从右向左输入4,这时候是3124,删除只能一个方向,是从左向右,依次是4213
关于双端队列的更多待续
关于栈的应用举例和栈的递归详见专栏数据结构和C语言实现数据结构