队列与循环队列
队列的定义
队列(Queue)是一种==“先进先出”==(First In First Out,FIFO)的线性表,只能允许队头出,队尾进。与现实生活中的排队类似。只能允许队头的人出去,不允许插队,因此想要排队就要从队尾排。
与栈(stack)不同的“先进后出”,即只运行在栈顶进行操作,不允许在栈底操作。类似于弹夹,先发射最上面那颗子弹(先进后出)
入队列的顺序:1->2->3->4->5->6
出队列的顺序:1->2->3->4->5->6
队列的创建
根据数据结构的知识,我们知道定义线性表有两种结构
顺序表:是在计算机内存中以数组的形式保存的线性表,是指用一组地址连续的存储单元依次存储数据元素的线性结构,使得线性表中在逻辑结构上相邻的数据元素存储在相邻的物理存储单元中,即通过数据元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系
由于顺序表在队列中并不多用,这里我使用链表的方式来创建队列,至于这两个结构创建的队列有什么区别,可以自行百度
创建队列
队列
typedef struct Queue
{
QNode* front;//队头
QNode* rear;//队尾
int size;//队列大小
}Queue;
创建保存队列内容的节点
typedef int QDataType;
typedef struct QListNode
{
struct QListNode* next;
QDataType data;
}QNode;
图解
队列的初始化
初始化很简单,就是一切从0开始嘛,这里可以断言(assert)来检查指针是否正确
//初始化队列
void QueueInit(Queue* q) {
assert(q);
//置空
q->front = q->rear = NULL;
q->size = 0;
}
队列的入队
向内存申请一个节点的空间,用队列的尾指针(rear)尾插这些创建的节点
注意:刚开始rear跟front都是指向NULL,因此要判断没有节点时,rear直接指向新节点,随后就是普普通通的尾插了
// 队尾入队列
void QueuePush(Queue* q, QDataType data) {
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL) {
exit(-1);
}
//插入
//1.插入数据
newnode->data = data;
newnode->next = NULL;
//2.没有节点时
if (q->rear == NULL) {
q->front = q->rear = newnod e;
}
else {
//尾插
q->rear->next = newnode;
//更新尾结点
q->rear = newnode;
}
//更新队列大小
q->size++;
}
图解
push->1,push->2,push->3,push->4,push->5,push->6
最后
判断队列是否为空
这里的返回值是布尔值(true or false),需要引
#include<stdbool.h>
头文件可以看成
if(q->front==NULL)
时return true
否则就return false
也要断言一下
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0
bool QueueEmpty(Queue* q) {
assert(q);
return q->front == NULL;
}
获取队列中有效元素个数
在
push
时,已经让size++
了,因此直接返回size
就OK了依旧断言一下
// 获取队列中有效元素个数
int QueueSize(Queue* q) {
assert(q);
return q->size;
}
获取头队列元素
获取前判断指针是否不为空,队列是否为空
返回头节点(队头)的数据即可
// 获取队列头部元素
QDataType QueueFront(Queue* q) {
assert(q);
assert(!QueueEmpty(q));
return q->front->data;
}
图解
这里也就是返回
1
获取队尾元素
跟获取队头元素一样
// 获取队列队尾元素
QDataType QueueBack(Queue* q) {
assert(q);
assert(!QueueEmpty(q));
return q->rear->data;
}
队列的出队
首先要断言,队列为空时,就不能出队了
这里有两种情况
1.当只有只有一个节点时
2.当有多个节点时
// 队头出队列
void QueuePop(Queue* q) {
assert(q);
//判断队是否空
assert(!QueueEmpty(q));
//1.只有一个元素时
if (q->front->next == NULL) {
free(q->front);
q->rear = q->front = NULL;
}
//2.多个节点
else {
QNode* next = q->front->next;
free(q->front);
q->front = next;
}
q->size--;
}
队列的销毁
跟单链表的销毁一样
用一个指针遍历节点一个个删除,删除完后把
front
跟rear
指针置空,把size
归零
// 销毁队列
void QueueDestroy(Queue* q) {
assert(q);
//记录队头地址
QNode* cur = q->front;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
//置空队头跟队尾指针
q->front = q->rear = NULL;
q->size = 0;
}
源代码
Queue.h
#pragma once
#pragma warning (disable:4996)
#include<stdio.h>
#include<malloc.h>
#include<assert.h>
#include<stdbool.h>
typedef int QDataType;
typedef struct QListNode
{
struct QListNode* next;
QDataType data;
}QNode;
// 队列的结构 指针集,通过这个可以访问QNode
typedef struct Queue
{
QNode* front;
QNode* rear;
int size;
}Queue;
// 初始化队列
void QueueInit(Queue* q);
// 队尾入队列
void QueuePush(Queue* q, QDataType data);
// 队头出队列
void QueuePop(Queue* q);
// 获取队列头部元素
QDataType QueueFront(Queue* q);
// 获取队列队尾元素
QDataType QueueBack(Queue* q);
// 获取队列中有效元素个数
int QueueSize(Queue* q);
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0
bool QueueEmpty(Queue* q);
// 销毁队列
void QueueDestroy(Queue* q);
Queue.c
#include "Queue.h"
//初始化队列
void QueueInit(Queue* q) {
assert(q);
//置空
q->front = q->rear = NULL;
q->size = 0;
}
// 销毁队列
void QueueDestroy(Queue* q) {
assert(q);
//记录队头地址
QNode* cur = q->front;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
//置空队头跟队尾指针
q->front = q->rear = NULL;
q->size = 0;
}
// 队尾入队列
void QueuePush(Queue* q, QDataType data) {
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL) {
exit(-1);
}
//插入
//1.插入数据
newnode->data = data;
newnode->next = NULL;
//2.没有节点时
if (q->rear == NULL) {
q->front = q->rear = newnode;
}
else {
//尾插
q->rear->next = newnode;
//更新尾结点
q->rear = newnode;
}
//更新队列大小
q->size++;
}
// 队头出队列
void QueuePop(Queue* q) {
assert(q);
//判断队是否空
assert(!QueueEmpty(q));
//1.只有一个元素时
if (q->front->next == NULL) {
free(q->front);
q->rear = q->front = NULL;
}
else {
QNode* next = q->front->next;
free(q->front);
q->front = next;
}
q->size--;
}
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0
bool QueueEmpty(Queue* q) {
assert(q);
return q->front == NULL;
}
// 获取队列中有效元素个数
int QueueSize(Queue* q) {
assert(q);
return q->size;
}
// 获取队列头部元素
QDataType QueueFront(Queue* q) {
assert(q);
assert(!QueueEmpty(q));
return q->front->data;
}
// 获取队列队尾元素
QDataType QueueBack(Queue* q) {
assert(q);
assert(!QueueEmpty(q));
return q->rear->data;
}
Test.c
#include "Queue.h"
void test1() {
Queue pq;
QueueInit(&pq);
QueuePush(&pq, 1);
QueuePush(&pq, 2);
QueuePush(&pq, 3);
QueuePush(&pq, 4);
QueuePush(&pq, 5);
}
int main() {
test1();
return 0;
}
循环队列
在介绍循环队列之前,先来解释一个概念
在顺序队列中,当尾指针移动到队列的边界时,即使队列前面的存储空间是空的,也会发生溢出,这种情况被称为“假溢出”
因此为了解决"假溢出"问题,充分利用内存空间,循环队列就出现了
循环队列通过将队列的存储空间视为一个循环的环形结构,使得队列的尾指针可以绕回到队列的头部继续存储数据,从而充分利用存储空间,避免了"假溢出"问题的发生。同时,循环队列的操作也相对简单,入队和出队操作都可以在固定的时间内完成,具有较高的效率
这里通过622. 设计循环队列 - 力扣(LeetCode)上的题目来讲解
设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
你的实现应该支持如下操作:
MyCircularQueue(k)
: 构造器,设置队列长度为 k 。Front
: 从队首获取元素。如果队列为空,返回 -1 。Rear
: 获取队尾元素。如果队列为空,返回 -1 。enQueue(value)
: 向循环队列插入一个元素。如果成功插入则返回真。deQueue()
: 从循环队列中删除一个元素。如果成功删除则返回真。isEmpty()
: 检查循环队列是否为空。isFull()
: 检查循环队列是否已满。示例:
MyCircularQueue circularQueue = new MyCircularQueue(3); // 设置长度为 3 circularQueue.enQueue(1); // 返回 true circularQueue.enQueue(2); // 返回 true circularQueue.enQueue(3); // 返回 true circularQueue.enQueue(4); // 返回 false,队列已满 circularQueue.Rear(); // 返回 3 circularQueue.isFull(); // 返回 true circularQueue.deQueue(); // 返回 true circularQueue.enQueue(4); // 返回 true circularQueue.Rear(); // 返回 4
提示:
- 所有的值都在 0 至 1000 的范围内;
- 操作数将在 1 至 1000 的范围内;
- 请不要使用内置的队列库。
看题目要求设计一个循环队列,且不能使用内置的队列库,也就是让我们造轮子嘛
构造循环队列
跟队列一样,构造时有两个结构可以选择
1.顺序表
2.链表
根据分析,创建循环队列用顺序表是最佳选择,为了更好的判断队列是否为空或者为满(与队列的有效元素有关),题目要求:
Rear
: 获取队尾元素。如果队列为空,返回 -1 。如果使用链表,front
跟rear
在节点中循环是很方便的,但是返回rear
节点时,由于队列的有效范围是[front,rear)
,因此rear
指针需要返回上一个节点才是真正的队尾,双向循环链表可以解决这个问题,结构过于复杂。而顺序表的rear
直接返回上一个位置即可,非常方便。
typedef struct {
int *a;
int rear;
int front;
int k;
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue *obj=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
//开k+1个空间
obj->a=(int*)malloc(sizeof(int)*(k+1));
obj->rear=obj->front=0;
obj->k=k;
return obj;
}
循环队列的判空
判空很简单,
front==rear
时队列为空。为了更直观观察循环队列的结果,我将环简化成以下结构
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
//重合时为空
return obj->rear==obj->front;
}
循环队列的判满
当队列是满的时,根据循环队列的特点,就是
rear
的下一个位置是front
就是满了,以下图为例1.
rear
的下一个rear+1=5
,咋一看rear
怎么越界了?不是说好的rear
的下一个是front
吗?为了解决这个问题,可以将
rear+1
的结果%(k+1)
防止越界,同时也解决了一个大问题,当rear
走到队列下标最大的地方时,对(k+1)
求余,结果就是0
。这个结论很重要,是循环队列的核心,请同学们时刻记住
2.当
pop->1,pop->2,pop->3
时,如下图
然后再
push->5,push->6,push->7
,如下图
此时是第二种队列满的情况
把下标套进上面的结论
(rear+1)%(K+1)==front
,(2+1)%(4+1)==3
刚好就是3
,无论是在那个地方队列满了,这个结论都可以完美解决这个问题(发明这个的一定是个天才)
bool myCircularQueueIsFull(MyCircularQueue* obj) {
//rear的下一个是front时为满
return (obj->rear+1)%(obj->k+1)==obj->front;
}
循环队列的入队
同样,入队列之前先要判满
满了就返回
false
,插入成功就返回true
(题目要求)先插入
value
在让rear++
才符合范围[front,rear)
因为是顺序表,
rear
指针如何返回到下标为0
的地方是个大问题,还记得上面的结论吗,直接套进去rear
的下标会发现,rear
到下标最大的位置后,对k+1
取余,就可以回到下标为0
的位置(对k+1
取余的本质就是限制rear
在这个范围内移动,解决越界的问题)因此,更新
rear
可每次对k+1
取余
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
//入队列
//先判满
if(myCircularQueueIsFull(obj))
return false;
obj->a[obj->rear]=value;
obj->rear++;
//更新rear
obj->rear %= obj->k+1;
return true;
}
循环队列的出队
出队跟入队类似,出去之前判空。
同样,上面那个结论可以用于
front
的更新
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return false;
//出队列
++obj->front;
//更新front
obj->front %= obj->k+1;
return true;
}
返回循环队列的队头
为空返回
-1
,不为空直接返回front
即可
int myCircularQueueFront(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return -1;
else
return obj->a[obj->front];
}
返回循环队列的队尾
以下图为例
获取队尾元素,就要将
rear
返回上一个位置这又涉及到另一个结论
(rear-1+(k+1))%(k+1)
,化简后(rear+k)%(k+1)
这个结论可以直接背下来,不理解的可以看下图,把
rear
下标直接代进去
int myCircularQueueRear(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return -1;
else
return obj->a[(obj->rear+obj->k)%(obj->k+1)];
}
循环队列的删除
用顺序表开了两个空间,删除时直接
free
即可
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->a);
free(obj);
}
源代码
typedef struct {
int *a;
int rear;
int front;
int k;
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue *obj=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
//开k+1个空间
obj->a=(int*)malloc(sizeof(int)*(k+1));
obj->rear=obj->front=0;
obj->k=k;
return obj;
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
//重合时为空
return obj->rear==obj->front;
}
bool myCircularQueueIsFull(MyCircularQueue* obj) {
//rear的下一个是front时为满
return (obj->rear+1)%(obj->k+1)==obj->front;
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
//入队列
//先判满
if(myCircularQueueIsFull(obj))
return false;
obj->a[obj->rear]=value;
obj->rear++;
//更新rear
obj->rear %= obj->k+1;
return true;
}
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return false;
//出队列
++obj->front;
//更新front
obj->front %= obj->k+1;
return true;
}
int myCircularQueueFront(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return -1;
else
return obj->a[obj->front];
}
int myCircularQueueRear(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return -1;
else
return obj->a[(obj->rear+obj->k)%(obj->k+1)];
}
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->a);
free(obj);
}
结语
本篇介绍了链队列、循环队列的食用方法,由于本人技术不足,如发现有BUG,可以私聊我解决