【数据结构篇C++实现】- 特殊的线性表 - 栈和队列

友情链接:C/C++系列系统学习目录


文章目录


🚀栈

故事导入:

我拼搏了几年后,终于有点小钱能买一辆车了,但是,我家住在巷子里,在一个胡同的尽头,而且整个胡同非常窄,只能容纳一辆车通过,而且是死胡同,我每天都要为停车发愁。当我回家早,把车停在胡同里面,早上上班一起来,我勒个去,我的车在胡同最里面,这到胡同口,一辆辆的车哇,我还要一辆一辆打电话,让胡同口那辆车出去,然后挨着一辆一辆出去,才能开到我的车。每天上班我都迟到,所以之后我干脆下班后再加班,晚点回来,等天黑了,别的车都停进去了再回家,再回去把车停在胡同口,这样早上就可以第一个去上班了。

 在这里插入图片描述
 
胡同里的小汽车是排成一条直线,是线性排列,而且只能从一端进出,后进的汽车先出去,后进先出(Last In First Out,简称LIFO),这就是"栈"。栈也是一种线性表,只不过它是操作受限的线性表,只能在一端操作

栈(Stack):是限定仅在表尾进行插入和删除操作的线性表。

  1. 首先栈是一种线性表,栈元素具有线性关系,即前驱和后继元素

  2. 是一种特殊的线性表,定义说在线性表的表尾进行插入和删除操作,这里表尾指栈顶,而不是栈底

  3. 栈的核心:

    ①栈顶(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;

解释:

  1. 栈底指针同时也就是我们分配空间的基地址
  2. 栈底指针也可改为使用数组ElemType base[MAXSIZE],但为了理解上方便,最好用指针分配空间
  3. 栈顶指针同样也不用设置为指针,因为是数组,定义为普通整形表示下标位置也可
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;

解释:

  1. 像之前一样,ElemType data[MAXSIZE];可用指针表示
  2. 栈0栈顶指针和栈1栈顶指针同样也能用指针表示
  3. 栈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),生活中队列场景随处可见: 比如在电影院, 商场, 或者厕所排队。。。。。。

队列:只允许在一端进行插入操作,而在另一端进行删除操作的线性表

  1. 队列是一种受限的线性结构

  2. 它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。

  3. 空队列:不包含任何元素的空表。

  4. 队列的核心:

    ①队头(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)【详解】
(排版结构等都借鉴了此位前辈的博客,对我的学习总结起到了很大的帮助)

  • 11
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈七.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值