友情链接:C/C++系列系统学习目录
🚀栈
故事导入:
我拼搏了几年后,终于有点小钱能买一辆车了,但是,我家住在巷子里,在一个胡同的尽头,而且整个胡同非常窄,只能容纳一辆车通过,而且是死胡同,我每天都要为停车发愁。当我回家早,把车停在胡同里面,早上上班一起来,我勒个去,我的车在胡同最里面,这到胡同口,一辆辆的车哇,我还要一辆一辆打电话,让胡同口那辆车出去,然后挨着一辆一辆出去,才能开到我的车。每天上班我都迟到,所以之后我干脆下班后再加班,晚点回来,等天黑了,别的车都停进去了再回家,再回去把车停在胡同口,这样早上就可以第一个去上班了。
胡同里的小汽车是排成一条直线,是线性排列,而且只能从一端进出,后进的汽车先出去,后进先出(Last In First Out,简称LIFO),这就是"栈"。栈也是一种线性表,只不过它是操作受限的线性表,只能在一端操作
栈(Stack):是限定仅在表尾进行插入和删除操作的线性表。
-
首先栈是一种线性表,栈元素具有线性关系,即前驱和后继元素
-
是一种特殊的线性表,定义说在线性表的表尾进行插入和删除操作,这里表尾指栈顶,而不是栈底
-
栈的核心:
①栈顶(Top):线性表允许进行插入删除的那一端。
②栈底(Base):固定的,不允许进行插入和删除的另一端。
🚢一、栈的顺序存储结构
⛳顺序栈
🎉(一)顺序栈的原理精讲
顺序栈:采用顺序存储的栈称为顺序栈,它利用一组地址连续的存储单元存放自栈底到栈顶的数据元素。其中,base 指向栈底,top 指向栈顶。
上面已经提到,栈只能在一端操作,后进先出,这是栈的关键特征,也就是说不允许在中间查找、取值、插入、删除等操作,我们掌握好顺序栈的初始化、入栈,出栈,取栈顶元素等操作即可。
区别说明:
本篇采用的方法,top指向的是下一个要插入的位置,即栈顶元素的下一个位置,而不是栈顶元素,在有些地方可以看见,top指向的就是栈顶的元素,在代码实现上有细微的差别,在具体位置我都会讲解
🎉(二)顺序栈的相关代码实现
1.栈的结构体定义
#define MaxSize 128 //预先分配空间,这个数值根据实际需要预估确定
typedef int ElemType;
typedef struct _SqStack{
ElemType *base; //栈底指针
ElemType *top; //栈顶指针
}SqStack;
//采用top指向栈顶元素的方法
#define MAXSIZE 50 //定义栈中元素的最大个数
typedef int ElemType; //ElemType的类型根据实际情况而定,这里假定为int
typedef struct{
ElemType data[MAXSIZE];
int top; //用于栈顶指针
}SqStack;
解释:
- 栈底指针同时也就是我们分配空间的基地址
- 栈底指针也可改为使用数组
ElemType base[MAXSIZE]
,但为了理解上方便,最好用指针分配空间 - 栈顶指针同样也不用设置为指针,因为是数组,定义为普通整形表示下标位置也可
2.栈的初始化
bool InitStack(SqStack &S) { //构造一个空栈 S
S.base = new int[MaxSize];//为顺序栈分配一个最大容量为 Maxsize 的空间
if (!S.base) return false; //空间分配失败
S.top=S.base; //top 初始为 base,空栈
return true;
}
//采用top指向栈顶元素的方法
bool StackEmpty(SqStack S){
S->top = -1; //初始化栈顶指针
}
解释:
采用top指向栈顶元素的方法时,并且以ElemType base[MAXSIZE]
方式分配空间,当栈存在一个元素时,top等于0,因此通常把空栈的判断条件定位top等于-1。初始化操作即为分配一个空栈,直接将top = -1即可
3.判断空栈
bool IsEmpty(SqStack &S){ //判断栈是否为空
if (S.top == S.base){
return true;
} else {
return false;
}
}
//采用top指向栈顶元素的方法
bool StackEmpty(SqStack S){
if(S.top == -1){
return true; //栈空
}else{
return false; //不空
}
}
解释:
采用top指向栈顶元素的方法时,当栈存在一个元素时,top等于0,因此通常把空栈的判断条件定位top等于-1。
4.判断栈满
//本篇方法
S.top-S.base == MaxSize;
//采用top指向栈顶元素的方法
S->top == MAXSIZE-1
解释:
指针的减法表示中间相隔多少个元素,基础知识不多讲
5.元素入栈
bool PushStack(SqStack &S, int e) { // 插入元素 e 为新的栈顶元素
if (S.top-S.base == MaxSize) //栈满
return false;
*(S.top++) = e; //元素 e 压入栈顶,然后栈顶指针加 1,等价于*S.top=e;S.top++;
return true;
}
//采用top指向栈顶元素的方法
/*插入元素e为新的栈顶元素*/
Status Push(SqStack *S, ElemType e){
//满栈
if(S->top == MAXSIZE-1){
return ERROR;
}
S->top++; //栈顶指针增加一
S->data[S->top] = e; //将新插入元素赋值给栈顶空间
return OK;
}
6.元素出栈
bool PopStack(SqStack &S, ElemType &e) //删除 S 的栈顶元素,暂存在变量 e中
{
if (S.base == S.top){ //栈空
return false;
}
e = *(--S.top); //栈顶指针减 1,将栈顶元素赋给 e
return true;
}
//采用top指向栈顶元素的方法
/*若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR*/
Status Pop(SqStack *S, ElemType *e){
if(S->top == -1){ //栈空
return ERROR;
}
*e = S->data[S->top]; //将要删除的栈顶元素赋值给e
S->top--; //栈顶指针减一
return OK;
}
解释:
出栈操作: 和入栈相反,出栈前要判断是否栈空,如果栈是空的,则出栈失败,否则将栈顶元素暂存给一个变量,栈顶指针向下移动一个空间(top–)。
7.获取栈顶元素
ElemType GetTop(SqStack &S) { //返回 S 的栈顶元素,栈顶指针不变
if (S.top != S.base){ //栈非空
return *(S.top - 1); //返回栈顶元素的值,栈顶指针不变
} else {
return -1;
}
}
//采用top指向栈顶元素的方法
/*读栈顶元素*/
Status GetTop(SqStack S, ElemType *e){
if(S->top == -1){ //栈空
return ERROR;
}
*e = S->data[S->top]; //记录栈顶元素
return OK;
}
⛳共享栈
🎉(一)共享栈原理精讲
共享栈:利用栈底位置相对不变的特征,可让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸
第一种方式:两个栈的栈顶指针都指向下一个可插入位置,S1.top=S1.base时1号栈为空,S2.top == S2.base == MaxSize时2号栈为空;仅当两个栈顶指针相同(S1.top = S2.top)时,此时能最后插入一个元素。当1号栈进栈时S1.top先赋值再加一,2号栈进栈时S2.top先赋值再减一出栈时则刚好相反。
第二种方式:两个栈的栈顶指针都指向栈顶元素,S1.top=-1时1号栈为空,S2.top=MaxSize时2号栈为空;仅当两个栈顶指针相邻(S1.top+1=S2.top)时,判断为栈满。当1号栈进栈时S1.top先加1再赋值,2号栈进栈时S2.top先减一再赋值出栈时则刚好相反。
🎉(二)共享栈相关代码实现
共享栈我们就以第二种方式再来讲解一下
1.共享栈的结构体定义
/*两栈共享空间结构*/
#define MAXSIZE 50 //定义栈中元素的最大个数
typedef int ElemType; //ElemType的类型根据实际情况而定,这里假定为int
/*两栈共享空间结构*/
typedef struct{
ElemType data[MAXSIZE];
int top0; //栈0栈顶指针
int top1; //栈1栈顶指针
}SqDoubleStack;
解释:
- 像之前一样,
ElemType data[MAXSIZE];
可用指针表示 - 栈0栈顶指针和栈1栈顶指针同样也能用指针表示
- 栈1的栈底指针就不需要了,其实就是MAXSIZE - 1
2.共享栈进栈
/*插入元素e为新的栈顶元素*/
bool Push(SqDoubleStack *S, Elemtype e, int stackNumber){
if(S->top0+1 == S->top1){ //栈满
return false;
}
if(stackNumber == 0){ //栈0有元素进栈
S->data[++S->top0] = e; //若栈0则先top0+1后给数组元素赋值
}else if(satckNumber == 1){ //栈1有元素进栈
S->data[--S->top1] = e; //若栈1则先top1-1后给数组元素赋值
}
return true;
}
解释:
对于两栈共享空间的push方法,我们除了要插入元素值参数外,还需要有一个判断是插入栈0还是栈1的栈号参数stackNumber。
3.共享栈出栈
/*若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR*/
bool Pop(SqDoubleStack *S, ElemType *e, int stackNumber){
if(stackNumber == 0){
if(S->top0 == -1){
return false; //说明栈0已经是空栈,溢出
}
*e = S->data[S->top0--]; //将栈0的栈顶元素出栈,随后栈顶指针减1
}else if(stackNumber == 1){
if(S->top1 == MAXSIZE){
return false; //说明栈1是空栈,溢出
}
*e = S->data[S->top1++]; //将栈1的栈顶元素出栈,随后栈顶指针加1
}
return true;
}
🚢二、栈的链式存储结构
⛳链栈
🎉(一)链栈的原理精讲
链栈:栈的链式存储结构就是链栈
想想看,栈是栈顶来做插入和删除操作,栈顶放在链表的头部还是尾部呢?由于单链表有头指针,而栈顶指针也是必须的,那干嘛不让它俩合二为一呢,所以比较好的办法就是把栈顶放在单链表头部,另外,都已经有了栈顶在头部了,单链表中比较常用的头结点也就失去了意义,通常对于链栈来说,是不需要头结点的
栈底就是链表的最后一个结点,而栈顶是链表的第一个结点,一个链栈可以由栈顶指针top唯一确定。链栈的元素入栈就类似于链表的头插法
- 链式存储结构可以更好的避免栈上溢,因为顺序栈在定义结构体时需要定义最大值,链栈除非内存已经没有可以使用的空间基本不存在栈满的情况
- 但对于空栈来说,链表原定义是头指针指向空,那么链栈的空其实就是top=NULL的时候。
- 通常采用单链表实现,并规定所有操作都是在单链表的表头进行的
- 这里规定链栈没有头节点,Lhead指向栈顶元素,
🎉(二)链栈的相关代码实现
1.链栈的结构体定义
//1.定义数据类型
typedef int ElemType;
//2.定义链栈结构体
typedef struct LinkStackNode
{
ElemType data;//存数据
struct LinkStackNode *next;//存下个节点的地址
} LinkStack;
解释:
也可以为链栈增加要素,例如增添一个count表示元素数量,但这样就不能跟结点共享一个结构体,需要单独定义:
/*栈的链式存储结构*/
/*构造节点*/
typedef struct LinkStackNode{
ElemType data;
struct StackNode *next;
}StackNode, *LinkStackPrt;
/*构造链栈*/
typedef struct LinkStack{
LinkStackPrt top;
int count;
}LinkStack;
//这样定义就不需要什么初始化操作了,最多初始化count = 0
2.链栈的初始化
//3.初始化链栈
int initLinkStack(LinkStack *L)
{
L = (LinkStack *) malloc(sizeof(LinkStack));//申请内存
if(!L->data) return 0;//申请失败
L->data = 0;//初始化链栈头结点数据域
L->next = NULL;//初始化链栈头结点指针域
return 1;
}
//采用分开定义结构体的方法:
3.链栈的入栈
//4.入栈
int push(LinkStack *L, ElemType e)
{
LinkStack *n;//新节点
n = (LinkStack *) malloc(sizeof(LinkStack));
if(!n->data) return 0;
n->data = e;//存入数据
n->next = L->next;//链栈栈顶元素链入新节点,新节点变成栈顶
L->next = n;//新节点链入链栈头结点末尾
return 1;
}
//采用分开定义结构体的方法:
/*插入元素e为新的栈顶元素*/
Status Push(LinkStack *S, ElemType e){
LinkStackPrt p = (LinkStackPrt)malloc(sizeof(StackNode));
p->data = e;
p->next = S->top; //把当前的栈顶元素赋值给新节点的直接后继
S->top = p; //将新的结点S赋值给栈顶指针
S->count++;
return OK;
}
4.链栈的出栈
//5.出栈
int pop(LinkStack *L, ElemType *e)
{
if(!L->next) return 0;//栈空,返回0
LinkStack *d = L->next;//出栈指针指向栈顶
*e = d->data;//赋值
L->next = d->next;//头结点跳过出栈节点,链入出栈节点的下一节点
free(d);//释放内存
return 1;
}
//采用分开定义结构体的方法:
/*若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR*/
Status Pop(LinkStack *S, ElemType *e){
LinkStackPtr 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;
}
5.取栈顶元素、遍历链栈
//取栈顶
int getTop(LinkStack *L, ElemType *e)
{
if(!L->next) return 0;
*e = L->next->data;
return 1;
}
//遍历链栈
void printStack(LinkStack *L)
{
LinkStack *p = L;//遍历指针
while (p)
{
p = p->next;
printf("%d ", p->data);
}
printf("\n");
}
其实操作跟链表差距不大,并且比链表的操作还少了很多,都只在链表头部操作
🚀队列
队列是一种受限的线性表,(Queue),它是一种运算受限的线性表,先进先出(First In First Out 缩写为 FIFO),生活中队列场景随处可见: 比如在电影院, 商场, 或者厕所排队。。。。。。
队列:只允许在一端进行插入操作,而在另一端进行删除操作的线性表
-
队列是一种受限的线性结构
-
它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。
-
空队列:不包含任何元素的空表。
-
队列的核心:
①队头(Front):允许删除的一端,又称队首。
②队尾(Rear):允许插入的一端。
🚢一、队列的顺序存储结构
⛳顺序队列
🎉(一)顺序队列原理精讲
顺序队列:分配一块连续的存储单元存放队列中的元素,采用数组来保存队列的元素
- 设立一个队首指针 front ,一个队尾指针 rear,队头指针 front 指向队头元素,队尾指针 rear 指向队尾元素的下一个位置。
- rear-front 即为存储的元素个数!
🎉(二)顺序队列相关代码实现
1.顺序队列的结构体定义
#define MaxSize 5 //队列的最大容量
typedef int DataType; //队列中元素类型
typedef struct Queue
{
DataType queue[MaxSize];
int front; //队头指针
int rear; //队尾指针
}SeqQueue;
2.队列的初始化
//队列初始化,将队列初始化为空队列
void InitQueue(SeqQueue *SQ) {
if(!SQ) return ;
SQ->front = SQ->rear = 0; //把对头和队尾指针同时置 0
}
3.判断队列是否为空、是否为满
//判断队列是否为空
int IsEmpty(SeqQueue *SQ)
{
if(!SQ) return 0;
if (SQ->front == SQ->rear)
{
return 1;
}
return 0;
}
//判断队列是否为满
int IsFull(SeqQueue *SQ)
{
if(!SQ) return 0;
if (SQ->rear == MaxSize)
{
return 1;
}
return 0;
}
4.元素入队
//入队,将元素 data 插入到队列 SQ 中
int EnterQueue( SeqQueue *SQ,DataType data) {
if(!SQ) return 0;
if(IsFull(SQ)){
cout<<"无法插入元素 "<<data<<", 队列已满!"<<endl;
return 0;
}
SQ->queue[SQ->rear] = data; //在队尾插入元素 data
SQ->rear++; //队尾指针后移一位
return 1;
}
5.元素出队
(1)删除front所指的元素,后面所有元素前移1并返回被删除元素
//出队,将队列中队头的元素 data 出队,后面的元素向前移动
int DeleteQueue(SeqQueue* SQ, DataType *data) {
if(!SQ || IsEmpty(SQ)){
cout<<"队列为空!"<<endl;
return 0;
}
if(!data) return 0;
*data = SQ->queue[SQ->front]; //返回被删除元素
for(int i=SQ->front+1; i<SQ->rear; i++) { //移动后面的元素
SQ->queue[i-1]=SQ->queue[i];
}
SQ->rear--;//队尾指针前移一位
return 1;
}
(2)删除front所指的元素,然后front指针加一并返回被删元素
//出队,将队列中队头的元素 data 出队,出队后队头指针 front 后移一位
int DeleteQueue2(SeqQueue* SQ,DataType* data) {
if (!SQ || IsEmpty(SQ))
{
cout<<"队列为空!"<<endl;
return 0;
}
if(SQ->front>=MaxSize){
cout<<"队列已到尽头!"<<endl;
return 0;
}
*data = SQ->queue[SQ->front]; //返回出队元素值
SQ->front = (SQ->front)+1; //队首指针后移一位
return 1;
}
解释:
以第二种方式删除元素,避免了元素的移动,但同时,因为只允许在队尾插入元素的限制,数组的可利用空间会不断减少
6.队列的遍历、取队首元素、清空队列
//打印队列中的各元素
void PrintQueue(SeqQueue* SQ) {
if(!SQ) return ;
int i = SQ->front;
while(i<SQ->rear)
{
cout<<setw(4)<<SQ->queue[i];
i++;
}
cout<<endl;
}
//获取队首元素
int GetHead(SeqQueue* SQ,DataType* data) {
if (!SQ || IsEmpty(SQ))
{
cout<<"队列为空!"<<endl;
}
return *data = SQ->queue[SQ->front];
}
//清空队列
void ClearQueue(SeqQueue* SQ)
{
SQ->front = SQ->rear = 0;
}
解释:
清空队列的操作,这里只是一个概念上的,我们直接将front和rear都置为0,满足我们一开始的定义,但实际上之前插入的元素都还存在,不过并不影响我们之后的操作,比如清空后再插入等等
⛳循环队列
🎉(一)循环队列原理精讲
在队列的顺序存储中,采用出队方式( 2), 删除 front 所指的元素,然后加 1 并返回被删元素。这样可以避免元素移动,但是也带来了一个新的问题“假溢出”。
怎么解决假溢出的问题呢,我们自然要思考能否利用前面的空间继续存储入队呢?答案是采用循环队列
循环队列:我们把队列头尾相接的顺序存储结构称为循环队列。
- 循环队列入队, 队尾循环后移: SQ->rear =(SQ->rear+1)%Maxsize;
- 循环队列出队, 队首循环后移: SQ->front =(SQ->front+1)%Maxsize;
- 队空:SQ.front=SQ.rear; // SQ.rear 和 SQ.front 指向同一个位置
- 队满: (SQ.rear+1) %Maxsize=SQ.front; // SQ.rear 向后移一位正好是 SQ.front
判断队列为满条件是 SQ.rear 向后移一位正好是 SQ.front的解释:
如以上操作,当队列满后,rear指向a2,与front指向相同,即SQ->front == SQ->rear,与判断队列是否为空的条件冲突,所以实际上我们一般最后留一个位置,判断当raer+1 = front时证明队列以满
所以a6实际上还是插入不进去的:
以上采用牺牲一个单元用来区分队空和队满,入队时少用一个队列单元,这是种较为普遍的做法,还有两种可以了解一下:
- 类型中增设表示元素个数的数据成员。这样,队空的条件为
Q->size == O
;队满的条件为Q->size == Maxsize
。这两种情况都有Q->front == Q->rear
- 类型中增设tag 数据成员,以区分是队满还是队空。tag 等于0时,若因删除导致
Q->front == Q->rear
,则为队空;tag 等于 1 时,若因插入导致Q ->front == Q->rear
,则为队满。
队列入队出队操作图示:
🎉(二)循环队列相关代码实现
1.循环队列的顺序存储结构
typedef int ElemType; //ElemType的类型根据实际情况而定,这里假定为int
#define MAXSIZE 50 //定义元素的最大个数
/*循环队列的顺序存储结构*/
typedef struct{
ElemType data[MAXSIZE];
int front; //头指针
int rear; //尾指针,若队列不空,指向队列尾元素的下一个位置
}SqQueue;
2.循环队列的初始化
//队列初始化,将循环队列初始化为空队列
void InitQueue(SeqQueue *SQ) {
if(!SQ) return ;
SQ->front = SQ->rear = 0; //把对头和队尾指针同时置 0
}
3.判断循环队列是否为空、是否为满
//判断队列为空
int IsEmpty(SeqQueue *SQ) {
if(!SQ) return 0;
if (SQ->front == SQ->rear)
{
return 1;
}
return 0;
}
//判断循环队列是否为满
int IsFull(SeqQueue *SQ) {
if(!SQ) return 0;
if ((SQ->rear+1)%MaxSize == SQ->front)
{
return 1;
}
return 0;
}
4.循环队列入队
//入队,将元素 data 插入到循环队列 SQ 中
int EnterQueue( SeqQueue *SQ,DataType data){
if(!SQ) return 0;
if(IsFull(SQ)){
cout<<"无法插入元素 "<<data<<", 队列已满!"<<endl;
return 0;
}
SQ->queue[SQ->rear] = data; //在队尾插入元素 data
SQ->rear=(SQ->rear+1)%MaxSize; //队尾指针循环后移一位
return 1;
}
5.循环队列出队
//出队,将队列中队头的元素 data 出队,出队后队头指针 front 后移一位
int DeleteQueue(SeqQueue* SQ,DataType* data) {
if (!SQ || IsEmpty(SQ))
{
cout<<"循环队列为空!"<<endl;
return 0;
}
*data = SQ->queue[SQ->front]; //出队元素值
SQ->front = (SQ->front+1)% MaxSize; //队首指针后移一位
return 1;
}
6.遍历循环队列、求循环队列长度
//打印队列中的各元素
void PrintQueue(SeqQueue* SQ) {
if(!SQ) return ;
int i = SQ->front;
while(i!=SQ->rear)
{
cout<<setw(4)<<SQ->queue[i];
i=(i+1)%MaxSize;
}
cout<<endl;
}
/*返回Q的元素个数,也就是队列的当前长度*/
int QueueLength(SqQueue SQ){
return (SQ.rear - SQ.front + MAXSIZE) % MAXSIZE;
}
🚢二、队列的链式存储结构
⛳链队列
🎉(一)链队列原理精讲
链队列:队列的链式存储结构表示为链队列,它实际上是一个同时带有队头指针和队尾指针的单链表,只不过它只能尾进头出而已。
区别说明:
本篇讲解采用front指向对头的方法;但在有些书中,将队头指针指向链队列的头结点,而队尾指针指向终端节点,在代码实现上有细微的差别,比如出队元素就要变为front -> next,两种方式在实现时我都会提到
🎉(二)链队列相关代码实现
1.链队列的结构体定义
typedef MaxSize //队列的最大容量
typedef int DataType; //队列中元素类型
//链队列的结点结构
typedef struct _QNode {
DataType data;
struct _QNode *next;
}QNode;
typedef QNode * QueuePtr;
//链队列
typedef struct Queue {
int length; //队列的长度
QueuePtr front; //队头指针
QueuePtr rear; //队尾指针
}LinkQueue;
2.链队列的初始化
//队列初始化,将队列初始化为空队列
void InitQueue(LinkQueue *LQ) {
LQ = (LinkQueue *)malloc(sizeof(LinkQueue)); //分配链队的内存
if(!LQ) return ;
LQ->length = 0;
LQ->front = LQ->rear = NULL; //把对头和队尾指针同时置 0
}
解释:
-
如果采用front指向头结点的方法,链队列初始化应该如此:front和rear要指向头结点
//初始化队列 void InitQueue(LinkQueue *Q) { //申请头结点内存空间 QueueNode *s = (QueueNode *)malloc(sizeof(QueueNode)); assert(s != NULL); //初始化时,将头指针和尾指针都指向头结点 Q->front = Q->tail = s; //将头结点的下一结点赋空 Q->tail->next = NULL; }
3.判断链队列是否为空、为满
//判断队列为空
int IsEmpty(LinkQueue *LQ) {
if(!LQ) return 0;
if (LQ->front == NULL)
{
return 1;
}
return 0;
}
//判断队列是否为满(链表除非内存全部用完,一般不存在为满的情况,这里我们手动设置了一个最大存储容量)
int IsFull(LinkQueue *LQ) {
if(!LQ) return 0;
if (LQ->length == MaxSize)
{
return 1;
}
return 0;
}
解释:
如果采用front指向头结点的方法,判断队列是否为空很简单:Q->front == Q->rear
相等则为空
4.链队列入队
int EnterQueue( LinkQueue *LQ,DataType data){
if(!LQ) return 0;
if(IsFull(LQ)){
cout<<"无法插入元素 "<<data<<", 队列已满!"<<endl;
return 0;
}
QNode *qNode = new QNode;
qNode->data = data;
qNode->next = NULL;
if(IsEmpty(LQ)){//空队列
LQ->front = LQ->rear = qNode;
}else {
LQ->rear->next =qNode;//在队尾插入节点 qNode
LQ->rear = qNode; //队尾指向新插入的节点
}
LQ->length++;
return 1;
}
解释:
-
同样采用front指向头结点方法的插入代码:
bool EnQueue(LinkQueue *Q, ElemType e){ LinkNode s = (LinkNode)malloc(sizeof(LinkNode)); s->data = e; s->next = NULL; Q->rear->next = s; //把拥有元素e新结点s赋值给原队尾结点的后继 Q->rear = s; //把当前的s设置为新的队尾结点 return false; }
5.链队列出队
//出队,将队列中队头的元素出队,其后的第一个元素成为新的队首
int DeleteQueue(LinkQueue *LQ, DataType *data){
QNode * tmp = NULL;
if(!LQ || IsEmpty(LQ)){
cout<<"队列为空!"<<endl;
return 0;
}
if(!data) return 0;
tmp = LQ->front;
LQ->front = tmp->next;
if(!LQ->front) LQ->rear=NULL;//如果对头出列后不存在其他元素,则rear 节点也要置空
*data = tmp->data;
LQ->length--;
delete tmp;
return 1;
}
解释:
-
同样给出front指向头结点的出队方法:
/*若队列不空,删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR*/ bool DeQueue(LinkQueue *Q, Elemtype *e){ LinkNode p; if(Q->front == Q->rear){ return false; } p = Q->front->next; //将欲删除的队头结点暂存给p *e = p->data; //将欲删除的队头结点的值赋值给e Q->front->next = p->next; //将原队头结点的后继赋值给头结点后继 //若删除的队头是队尾,则删除后将rear指向头结点 if(Q->rear == p){ Q->rear = Q->front; } free(p); return true; }
其它像遍历打印链队列元素,获得队首元素,清空队列等就不一一讲解了,操作都比较简单
🚢三、队列的拓展
⛳拓展1:双端队列
双端队列是指允许两端都可以进行入队和出队操作的队列,如下图所示。其元素的逻辑结构仍是线性结构。将队列的两端分别称为前端和后端,两端都可以入队和出队。
其元素的逻辑结构仍然是线性结构,可以采用顺序存储,也可以采用链式存储。(顺序双端队列和链式双端队列)
几种特殊的双端队列:
- 输入受限的双端队列:允许在一端进行插入和删除,但在另一端只允许删除的双端队列称为输入受限的双端队列,若限定双端队列从某个端点插入的元素只能从该端点删除,则该双端队列就蜕变为两个栈底相邻接的栈。
- 输出受限的双端队列:允许在一端进行插入和删除,但在另一端只允许插入的双端队列称为输出受限的双端队列,
⛳扩展2:优先队列
英雄联盟游戏里面防御塔都有一个自动攻击功能,小兵排着队进入防御塔的攻击范围,防御塔先攻击靠得最近的小兵,这时候大炮车的优先级更高(因为系统判定大炮车对于防御塔的威胁更大),所以防御塔会优先攻击大炮车。而当大炮车阵亡,剩下的全部都是普通小兵,这时候离得近的优先级越高,防御塔优先攻击距离更近的小兵。
优先队列: 它的入队顺序没有变化,但是出队的顺序是根据优先级的高低来决定的。优先级高的优先出队
typedef int DataType; //队列中元素类型
typedef struct _QNode { //结点结构
int priority; //每个节点的优先级,9 最高优先级,0 最低优先级,优先级相同,取第一个节点
DataType data;
struct _QNode *next;
}QNode;
typedef QNode * QueuePtr;
typedef struct Queue {
int length; //队列的长度
QueuePtr front; //队头指针
QueuePtr rear; //队尾指针
}LinkQueue;
⛳扩展3:线性池中的任务队列
线程池 - 由一个任务队列和一组处理队列的线程组成。一旦工作进程需要处理某个可能“阻塞”的操作,不用自己操作,将其作为一个任务放到线程池的队列,接着会被某个空闲线程提取处理。
⛳扩展4:双向循环队列(高并发web服务器开发)
在高并发 HTTP 反向代理服务器 Nginx 中,存在着一个跟性能息息相关的模块 - 文件缓存。经常访问到的文件会被 nginx 从磁盘缓存到内存,这样可以极大的提高 Nginx 的并发能力,不过因为内存的限制,当缓存的文件数达到一定程度的时候就会采取淘汰机制,优先淘汰进入时间比较久或是最近访问很少(LRU)的队列文件
具体实现方案:
使用双向循环队列保存缓存的文件节点,这样可以实现多种淘汰策略:比如:如果采用淘汰进入时间比较久的策略,就可以使用队列的特性,先进先出如果要采用按照 LRU,就遍历链表,找到节点删除
总结参考资料:
程杰:大话数据结构
严蔚敏:数据结构C语言版
数据结构:线性表(List)【详解】
(排版结构等都借鉴了此位前辈的博客,对我的学习总结起到了很大的帮助)