文章目录
- 前言
- 一、队列的概念
- 二、队列的实现
- Queue.h
- Queue.c
- 三、设计循环队列问题
- 数组实现
- 链表实现
- 总结
前言
嗨喽喽!!小伙伴们,大家好哇,欢迎来到我的博客!
今天将要分享的是另一种数据结构—队列,以及与队列具有相通之处的一种结构,循环队列的实现。
一、队列的概念
队列(Queue):只能在一端进行插入数据操作,在另一端进行删除数据的特殊线性表。与栈相反的是,队列遵循先进先出 FIFO(First In First Out)的原则,进行插入数据的一端称之为队尾(入队),进行删除数据的一端则为队头(出队)。
看到队列这一数据结构的名字与以上对于队列的概念的说明,相信小伙伴们脑海中早已浮现日常生活中与此结构相类似的场景。就是我们平时购买物品或办理业务时,通常会采取排队的方式确保秩序与公平,先入队的人先出队。
二、队列的实现
讲述完了队列的相关概念,接下来便可以着手实现队列及其有关的基本操作了!!
队列既可以使用数组也可以使用链表来实现。但是总体来说,使用链表实现更优,因为使用数组在队头出数据,其效率会低很多。
所以我们使用链表来实现一个队列:
Queue.h
首先时队列头文件的头文件包含与链表和队列的声明:
typedef int QDataType;
typedef struct QListNode
{
struct QListNode* _pNext;//指向队列的下一个
QDataType _data;
}QNode;
typedef struct Queue
{
QNode* _front;//指向队头
QNode* _rear;//指向队尾
int _size;
}Queue;
然后是,队列中的一些相关的方法的声明:
//初始化队列
void QueueInit(Queue* q);
//销毁队列
void QueueDestroy(Queue* q);
//入队(队尾)
void QueuePush(Queue* q, QDataType x);
//出队(队头)
void QueuePop(Queue* q);
//获取队头元素
QDataType QueueFront(Queue* q);
//获取队尾元素
QDataType QueueBack(Queue* q);
//队列判空
bool QueueEmpty(Queue* q);
//获取队列元素个数
int QueueSize(Queue* q);
Queue.c
接下来便是队列中的一些方法的实现:
首先是队列的初始化与销毁,销毁操作类似于链表,通过遍历对节点逐个释放:
void QueueInit(Queue* q)
{
assert(q);
q->_front = q->_rear = NULL;
q->_size = 0;
}
void QueueDestroy(Queue* q)
{
assert(q);
QNode* pcur, * next;
pcur = q->_front;
while (pcur)
{
next = pcur->_pNext;
free(pcur);
pcur = next;
}
q->_front = q->_rear = NULL;
q->_size = 0;
}
然后是队列的入队操作,在入队之前需要先检查队列是否为空:
void QueuePush(Queue* q, QDataType x)
{
assert(q);
QNode* node = (QNode*)malloc(sizeof(QNode));
if (node == NULL)
{
perror("malloc fail!");
exit(1);
}
node->_data = x;
node->_pNext = NULL;
if (q->_rear)//队列不为空
{
q->_rear->_pNext = node;
}
else//队列为空
{
q->_front = node;
}
q->_rear = node;
q->_size++;
}
使用动画解释入队操作:
然后是出队操作,此处则存在队列为一个元素与多个元素的情况:
void QueuePop(Queue* q)
{
assert(q && q->_size);
if (q->_front->_pNext)//队列不仅一个元素
{
QNode* next = q->_front->_pNext;
free(q->_front);
q->_front = NULL;
q->_front = next;
}
else//队列仅一个元素
{
free(q->_front);
q->_front = q->_rear = NULL;
}
q->_size--;
}
使用动画解释出队操作:
然后是获取队头与队尾元素:
QDataType QueueFront(Queue* q)
{
assert(q && q->_front);
return q->_front->_data;
}
QDataType QueueBack(Queue* q)
{
assert(q && q->_rear);
return q->_rear->_data;
}
最后则是队列的判空与获取当前队列的大小的操作:
bool QueueEmpty(Queue* q)
{
assert(q);
return q->_size == 0;
}
int QueueSize(Queue* q)
{
assert(q);
return q->_size;
}
三、设计循环队列问题
分享完了队列的相关内容,接下来让我们看一个与队列有一定相通之处的设计循环队列的问题。
首先给出力扣上的相关题目的链接:【设计循环队列】。
当然,这个循环队列既可以使用数组也可以使用链表来实现,由于难度基本相差不大。这边主要使用数组来讲解实现过程,同时在最后也会给出使用链表实现的相关代码。
数组实现
首先通过题目我们可以知道循环队列的逻辑结构大致类似于下图:
但是他的物理结构上确实一个数组,不可能将首尾相接。那么这里我们便可以通过对tail用size取模。当tail在数组末尾时再加一,那么肯定会造成数组越界,此时我们就可以使用size对tail进行取模,tail便成了0,指向了数组首位,如此便形成了循环,当然队头也可以使用相类似的操作。如下图所示,tail开始为5,size为6,此时队列入数据,对tail加1,我们对tail取模,tail就等于了0:
但此时,我们会存在两种特殊情况,那便是数组的空与满。数组为空还可以放入元素,为满那肯定不可以再存入元素。这时,可能部分小伙伴会说,判断此时队头与队尾指向的位置是否相同,来区分空与满。但要知道的是空与满时队头与队尾指向的位置其实都是相同的。
其实此时我们可以采取两种方法来解决上述的问题:
1、使用一个size来记录当前的数组元素的个数,当元素的个数与循环队列的长度k相等时,不就为满了,size==0不就为空了。
2、我们可以实际上创建k+1个长度循环队列,然后让tail一直指向末尾元素的下一个下标的位置,即让数组中始终有一个位置不存放数据。此时队头与队尾指向同一个位置时,即为空;队尾的下一个为队头就为满:
那我们就使用第一种方法来手撕一个循环队列吧!!
首先是循环队列的声明:
typedef struct {
int* a;
int head;
int tail;
int size;//判断满与空的特殊情况
int k;
} MyCircularQueue;
然后是循环队列的初始化,一开始头与尾都指向数组的开始位置:
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* pcq = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
pcq->a = (int*)malloc(k * sizeof(int));
pcq->size = pcq->head = pcq->tail = 0;
pcq->k = k;
return pcq;
}
循环队列的入队操作,入队之前需要先判断队列是否为满,即size与k相等时即为满,然后在对tial加加了之后,则需要使用k对tail取模,形成循环:
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if (obj->size == obj->k)//循环队列为满
{
return false;
}
obj->a[obj->tail] = value;
obj->tail++;
obj->tail %= obj->k;
obj->size++;
return true;
}
循环队列的出队操作,出队之前则需要判断队列是否为空(即size==0),不为空,就直接让head++。同样的,在对head++后也需要使用k对head进行取模操作:
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if (obj->size == 0)//循环队列为空
{
return false;
}
obj->head++;
obj->head %= obj->k;
obj->size--;
return true;
}
然后是循环队列的取队头数据的操作,直接返回head指向的数据即可:
int myCircularQueueFront(MyCircularQueue* obj) {
if (obj->size == 0)
return -1;
return obj->a[obj->head];
}
取循环队列的尾数据就相对复杂了,因为tail的前一个元素为尾元素。那么,当tail==0时,尾元素在数组的末尾位置。当然,有一种简单的解决方法:使用三目运算符,即判断tail是否指向0,是就返回数组末尾元素(即k - 1的位置),否就只返回tail - 1指向的元素即可。
此处同时还有一种更为🐂🍺(NB)的写法,就是返回使用k对tail取模位置的元素。即让tail - 1 + k在使用k对结果进行取模操作,此时的值便指向了循环队列的末尾元素。比如tail为0时,减1加k再用k取模,值为k - 1:
int myCircularQueueRear(MyCircularQueue* obj) {
if (obj->size == 0)
return -1;
return obj->tail == 0 ? obj->a[obj->k - 1] : obj->a[obj->tail - 1];
//return obj->a[(obj->tail - 1 + obj->k) % obj->k];//更装逼的写法
}
接下来就是循环队列的判空与判满操作,非常简单,就不加赘述:
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->size == 0;
}
bool myCircularQueueIsFull(MyCircularQueue* obj) {
return obj->size == obj->k;
}
最后则是循环队列的销毁操作:
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->a);
obj->a = NULL;
obj->size = obj->k = obj->head = obj->tail = 0;
free(obj);
}
代码写完,直接提交完事:
链表实现
最后再给出使用链表实现循环队列,并采用对循环队列多创建一个位置的方法进行判断循环队列的空与满的代码。
使用链表是实现就不需要取模来让队列循环了,只需要让链表的首节点与尾节点相连接形成循环链表即可。基本实现逻辑大致相同,就是判空与判满操作存在不同,这里直接上代码:
typedef struct LTNode
{
int x;
struct LTNode* next;
}LTNode;
//用链表实现
typedef struct {
LTNode* phead;
LTNode* ptail;
int k;
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
obj->phead = (LTNode*)malloc(sizeof(LTNode));
obj->ptail = obj->phead;
for (int i = 0; i < k; i++)//多创建一个节点判断空与满的情况
{
obj->ptail->next = (LTNode*)malloc(sizeof(LTNode));
obj->ptail = obj->ptail->next;
}
obj->ptail->next = obj->phead;
obj->ptail = obj->phead;
obj->k = k + 1;
return obj;
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
//循环队列头节点与尾节点相同时,队列为空
return obj->phead == obj->ptail;
}
bool myCircularQueueIsFull(MyCircularQueue* obj) {
//循环队列尾节点的下一个节点为头节点时,队列为满
return obj->ptail->next == obj->phead;
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if (myCircularQueueIsFull(obj))
return false;
obj->ptail->x = value;
obj->ptail = obj->ptail->next;
return true;
}
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj))
return false;
obj->phead = obj->phead->next;
return true;
}
int myCircularQueueFront(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj))
return -1;
return obj->phead->x;
}
int myCircularQueueRear(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj))
return -1;
LTNode* tail = obj->phead;
while (tail->next != obj->ptail)
tail = tail->next;
return tail->x;
}
void myCircularQueueFree(MyCircularQueue* obj) {
LTNode* pcur = obj->phead;
for (int i = 0; i < obj->k; i++)
{
LTNode* next = pcur->next;
free(pcur);
pcur = NULL;
pcur = next;
}
free(obj);
}
最后提交走人:
总结
以上便是有关队列的相关知识的分享。如果,小伙伴们觉得有帮助的话,点个赞是最好的!ο(=•ω<=)ρ⌒☆