【数据结构】队列的实现

embedded/2024/9/23 11:19:22/

0. 前言

上期博客给大家讲解了 栈 以及 栈的实现,今天再给大家讲一个特殊的顺序表结构,那就是队列

下面就进入正题!一起学习一下吧!

1. 队列

1.1 队列的概念

队列只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,

队列具有先进先出FIFO (First In First Out)

入队列:进行插入操作的一端称为队尾

出队列:进行删除操作的一端称为队头

其实队列,换一种说法就相当于我们生活中的 排队问题,不管干什么一般总是遵守先来后到的,
就是先来的(对头)先获取到资源,后来的不准插队,只能在最后面(队尾)排队等待。

1.2 队列的结构

它的结构如图所示:
 

2. 队列的实现 

和栈的实现类似的想法,我们实现队列,既可以用数组实现,也可以用链表的结构实现。

如下图所示:

但是我们需要考虑的是,之前学习的哪一种结构可以更高效的实现我们的队列呢?

其实我们对比顺序表和链表的优缺点,

链表:空间的利用率高,不用扩容,头插头删高效

顺序表:空间利用率低,需要扩容,但是尾插尾删效率高

针对队列的结构,我们发现

因为如果使用数组的结构,出队列在数组头上出数据,也就是对顺序表进行删除的操作时,

需要挪动数据。效率是比较低的。 

所以我们会选择使用链表来进行 队列的实现。

2.1 准备工作

还是像往常一样,我们将队列其拆分为不同的文件进行设计

1️⃣:Queue.h 文件,用于函数声明
2️⃣:Queue.c 文件,用于函数的定义

3️⃣:Test.c   文件,用于测试函数

2.2  队列 结构体的定义

队列的结构体定义跟链表结构体定义类似:

//队列节点 
typedef struct QueueNode
{struct QueueNode* next;//下一个节点的地址QDataType val;//节点存的数据
}QNode;

除了节点定义之外,我们可以再定义一个队列的结构体类型,

队列的结构体由三个部分组成:

  • phead是一个指向QNode结构体的指针,用于指向队列的队头元素。
  • ptail也是一个指向QNode结构体的指针,用于指向队列的队尾元素。
  • size表示队列中当前元素的个数。
typedef struct Queue
{QNode* phead;//队头指针QNode* ptail;//队尾指针int size;//队列中的元素个数
}Queue;

有细心的同学会很好奇,

为什么我们还需要定义一个Queue这样的结构体类型呢?

队头和队尾定义在刚才定义在QueueNode这个队列节点结构体类型不好吗?

也许是这样的结构体设计

//队列节点 
typedef struct QueueNode
{struct QueueNode* next;//下一个节点的地址QDataType val;//节点存的数据
}QNode, *phead, *ptail;
//对struct QueueNode 重命名为QNode,//根据队列节点类型,定义的队头指针phead和队尾指针ptail

能想到这样问题的同学,很优秀!👍👍👍

其实我们在定义链式队列结构体,也不是非得必须有如上这两个结构体的,

只不过有上面这两个结构体只是在调用函数时方便参数传递

我们在实现链表中也遇到过,对于链表的操作,插入删除操作时,指向链表的头指针可能会发生改变,我们在函数调用时,参数需要用二级指针接收。

这里也是同样的,在队列中插入元素,或者删除时,我们的队头指针phead和队尾指针rear会发生改变。我们在函数调用时,参数同样需要用二级指针接收。同学们会很容易犯同样的错误~

但如果再构造一个队列结构体Queue 我们的队头指针和队尾指针就是我们的结构体成员,

无论Queue这个类型有多少指向结点的指针,直接传递一个指向链队结点的类型的指针就可以了,

这样Queue这个类型这些指向结点的指针一并就传入了,如果想使用,我们可以通过结构体指针访问操作符,也就是这个箭头“->”访问,你看这样是不是就很方便了?

typedef int QDataType;typedef struct QueueNode
{struct QueueNode* next;QDataType val;
}QNode;typedef struct Queue
{QNode* phead;QNode* ptail;int size;
}Queue;

这是我们在实现链式队列的一个小优化方式,同学们是不是get到了?是不是很妙呀?

2.3 队列初始化

初始化就是将记录的头指针和尾指针置空,元素个数置暂时赋为0

代码如下:

void QueueInit(Queue* pq)
{assert(pq);pq->phead = NULL;pq->ptail = NULL;pq->size = 0;
}

2.4 队尾插入

进行入队操作的时候,我们首先需要判断一下队列是否为空,

如果队列为空的话,需要将头指针和尾指针一同指向第一个结点,

当如果队列不为空的时候,入队列时,我们可以创建一个新节点,并放在记录好的尾节点的位置之后,成为新的尾节点,最后元素个数增加。

代码如下:

// 队尾插入
void QueuePush(Queue* pq, QDataType x)
{assert(pq);//创建新节点QNode* newnode = (QNode*)malloc(sizeof(QNode));if (newnode == NULL){perror("malloc fail");return;}newnode->next = NULL;newnode->val = x;//队列为空if (pq->ptail == NULL){pq->phead = pq->ptail = newnode;}//队列不为空else{pq->ptail->next = newnode;pq->ptail = newnode;}pq->size++;//插入数据之后不要忘了把元素个数+1
}

2.5 队头删除

我们分以下情况讨论,

在队列不为空的情况下,进行一个判断,如图所示,如果队列只有一个元素了(即头尾指针均指向了同一个结点),直接将头尾两指针制空(NULL)并释放这一个结点即可。  

当队列含有2个以上元素时,我们需要将队列的头指针指向头指针当前指向的下一个元素并释放掉当前元素即可。最后元素个数减少。如图所示

代码如下:

void QueuePop(Queue* pq)
{assert(pq);assert(pq->size != 0);// 一个节点if (pq->phead->next == NULL){free(pq->phead);pq->phead = pq->ptail = NULL;}else // 至少2个节点{QNode* next = pq->phead->next;free(pq->phead);pq->phead = next;}pq->size--;
}

 2.6 返回队头元素

返回我们记录的头节点的值即可。

代码如下:

QDataType QueueFront(Queue* pq)
{assert(pq);assert(pq->phead);return pq->phead->val;
}

2.7  返回队尾元素

返回我们记录的尾节点的值即可。

代码如下:

QDataType QueueBack(Queue* pq)
{assert(pq);assert(pq->ptail);return pq->ptail->val;
}

2.8 队列元素个数

返回我们记录的元素个数即可。

代码如下:

int QueueSize(Queue* pq)
{assert(pq);return pq->size;
}

2.9 队列判空

返回我们记录的元素个数是否为零。

代码如下:

bool QueueEmpty(Queue* pq)
{assert(pq);return pq->size == 0;
}

2.10 队列销毁

类似于单链表的销毁,依次销毁每一个节点后将头尾置空,将元素个数置空。

代码如下:

void QueueDestroy(Queue* pq)
{assert(pq);QNode* cur = pq->phead;while (cur){QNode* next = cur->next;free(cur);cur = next;}pq->phead = pq->ptail = NULL;pq->size = 0;
}

3. 完整代码:

Queue.h

#pragma once#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>typedef int QDataType;typedef struct QueueNode
{struct QueueNode* next;QDataType val;
}QNode;typedef struct Queue
{QNode* phead;QNode* ptail;int size;
}Queue;void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);// 队尾插入
void QueuePush(Queue* pq, QDataType x);
// 队头删除
void QueuePop(Queue* pq);// 取队头和队尾的数据
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);int QueueSize(Queue* pq);
bool QueueEmpty(Queue* pq);

Queue.c

#define _CRT_SECURE_NO_WARNINGS 1#include"Queue.h"void QueueInit(Queue* pq)
{assert(pq);pq->phead = NULL;pq->ptail = NULL;pq->size = 0;
}void QueueDestroy(Queue* pq)
{assert(pq);QNode* cur = pq->phead;while (cur){QNode* next = cur->next;free(cur);cur = next;}pq->phead = pq->ptail = NULL;pq->size = 0;
}// 队尾插入
void QueuePush(Queue* pq, QDataType x)
{assert(pq);QNode* newnode = (QNode*)malloc(sizeof(QNode));if (newnode == NULL){perror("malloc fail");return;}newnode->next = NULL;newnode->val = x;if (pq->ptail == NULL){pq->phead = pq->ptail = newnode;}else{pq->ptail->next = newnode;pq->ptail = newnode;}pq->size++;
}// 队头删除
void QueuePop(Queue* pq)
{assert(pq);assert(pq->size != 0);/*QNode* next = pq->phead->next;free(pq->phead);pq->phead = next;if (pq->phead == NULL)pq->ptail = NULL;*/// 一个节点if (pq->phead->next == NULL){free(pq->phead);pq->phead = pq->ptail = NULL;}else // 多个节点{QNode* next = pq->phead->next;free(pq->phead);pq->phead = next;}pq->size--;
}QDataType QueueFront(Queue* pq)
{assert(pq);assert(pq->phead);return pq->phead->val;
}QDataType QueueBack(Queue* pq)
{assert(pq);assert(pq->ptail);return pq->ptail->val;
}int QueueSize(Queue* pq)
{assert(pq);return pq->size;
}bool QueueEmpty(Queue* pq)
{assert(pq);return pq->size == 0;
}

test.c

#include"Queue.h"int main()
{Queue q;QueueInit(&q);QueuePush(&q, 1);QueuePush(&q, 2);printf("%d ", QueueFront(&q));QueuePop(&q);QueuePush(&q, 3);QueuePush(&q, 4);while (!QueueEmpty(&q)){printf("%d ", QueueFront(&q));QueuePop(&q);}printf("\n");return 0;
}
代码运行界面

 以上就是对于数据结构队列的基本实现,

4. 总结

这期我们学习的队列和上期博客学习的栈,其实有很多相似之处,尽管栈是队头进入删除数据(后进先出),队列是队尾入数据,队头删数据(先进先出),但其本质是一样的。我们用顺序表实现栈,用链表实现的队列,希望大家对于前面的顺序表和链表的理解更加深刻哦~

 以上就是本期博客的全部内容,如果有疑惑,有问题的小伙伴,欢迎评论区和我探讨哦!

同时我还会继续更新数据结构更多的知识,分享给更多小伙伴,请继续关注我哦!😄😄


http://www.ppmy.cn/embedded/97970.html

相关文章

全网最详细Linux安装openJDK教程

目录 前言&#xff1a;这看似很简单的jdk环境变量配置&#xff0c;但是里面很多坑&#xff0c;有些安装包可能是错误的&#xff0c;倒是环境变量配置正确&#xff0c;但是环境没生效。 1.找到正确的JDK版本 Index of /Adoptium/8/jdk/x64/linux/ | 清华大学开源软件镜像站 | T…

如何挑选高性价比蓝牙耳机?四款2024出众耳机品牌盘点推荐!

在数字化时代&#xff0c;蓝牙耳机已成为我们日常生活中不可或缺的一部分。无论是通勤路上、运动时&#xff0c;还是工作学习中&#xff0c;一款好的蓝牙耳机总能给我们带来极致的音乐体验。然而&#xff0c;面对市面上琳琅满目的产品&#xff0c;在预算有限的情况下如何挑选高…

Linux中nano编辑器详解

nano 是一个简单的文本编辑器&#xff0c;通常预装在大多数 Linux 发行版中。它非常适合初学者使用&#xff0c;因为它有一个用户友好的界面和易于理解的命令集。下面是对 nano 编辑器的详细说明。 启动 nano 要启动 nano 并打开一个文件进行编辑&#xff0c;你可以在终端中输…

【AI绘画】如何选择AI绘画工具?Midjourney VS Stable Diffusion

文章目录 &#x1f4af;如何选择合适的AI绘画工具个人需求选择比较工具特点社区和资源 &#x1f4af; Midjourney VS Stable Diffusion&#xff1a;深度对比与剖析使用费用对比使用便捷性与系统兼容性对比开源与闭源对比图片质量对比上手难易对比学习资源对比作品版权问题产品定…

基于BlockingQueue的生产者消费者模型

文章目录 引言理解生产者消费者模型基于BlockingQueue的生产者消费者模型单生产&#xff0c;单消费模型多生产、多消费模型 引言 生产者消费者模型一般可以在超市中听到&#xff0c;例如如下是一个专门卖方便面的超市&#xff0c;这个超市有自己供应商&#xff0c;也有客户来买…

鸿蒙内核源码分析(寄存器篇) | 宇宙最忙存储器

寄存器的本质 寄存器从大一的计算机组成原理就开始听到它&#xff0c;感觉很神秘&#xff0c;如梦如雾多年.揭开本质后才发现&#xff0c;寄存器就是一个32位的存储空间&#xff0c;一个int变量而已&#xff0c;但它的厉害之处在于极高频率的使用&#xff0c;让人不敢相信是怎…

白酒品鉴的艺术:品味与感悟的很好结合

白酒&#xff0c;作为中国千年的文化瑰宝&#xff0c;其品鉴不仅是一种味觉上的享受&#xff0c;更是一种精神上的追求。在品鉴白酒的过程中&#xff0c;品味与感悟如同双翼&#xff0c;共同构筑起一场艺术与文化的盛宴。今天&#xff0c;我们就来探讨一下白酒品鉴的艺术&#…

什么是线程和应用?线程和进程区别是什么?

一、线程和应用的定义 1. 线程&#xff1a; • 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中&#xff0c;是进程中的实际运作单位。 • 一个线程指的是进程中一个单一顺序的控制流&#xff0c;一个进程中可以并发多个线程&#xff0c;每条线程并行执行不同…