上一节中已经向大家介绍了栈的两种实现方法,今天我们来学习另一种线性存储结构--队列。先赞后看是好习惯!!!
目录
一、队列的概念及结构
队列:只允许在一端插入数据,在另一端删除数据的特殊线性表,队列具有先进先出FIFO(First In First Out) 原则。
入队列:进行插入操作的一端称为队尾。
出队列:进行删除操作的一端称为队头。
二、用链表实现队列
为了实现队列,我们需要一种数据结构,可以在一端添加元素,并在另一端删除元素,链表和数组都符合要求。那我们先来试试用链表实现队列。
首先在头文件中声明队列的结果以及相关功能接口。
//Queue.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int QDataType;
typedef struct QueueNode
{
int val;
struct QueueNode* next;
}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);
//判断队列是否为空
bool QueueEmpty(Queue* pq);
//计算队列大小
int QueueSize(Queue* pq);
1. 队列的初始化和销毁
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;
}
2. 入队列
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc");
exit(1);
}
newnode->val = x;
newnode->next = NULL;
if (pq->ptail)
{
//队列里已经有元素了
pq->ptail->next = newnode;
pq->ptail = newnode;
}
else
{
//插入的是队列的第一个元素
pq->phead = pq->ptail = newnode;
}
pq->size++;
}
3. 出队列
void QueuePop(Queue* pq)
{
assert(pq);
//温柔检查
/*if (pq->phead == NULL)
return;*/
//暴力检查(推荐)
assert(pq->phead != 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--;
}
4. 取队头
QDataType QueueFront(Queue* pq)
{
assert(pq);
// 暴力检查
assert(pq->phead != NULL);
return pq->phead->val;
}
5. 取队尾
QDataType QueueBack(Queue* pq)
{
assert(pq);
// 暴力检查
assert(pq->ptail != NULL);
return pq->ptail->val;
}
6. 判断队列是否为空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->size == 0;
}
7. 计算队列大小
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
三、用数组实现队列
我们可以使用一个变量front指向队首元素的索引,并维护一个变量size用于记录队列长度。定义rear=front+size ,这个公式计算出的rear指向队尾元素之后的下一个位置。
基于此设计,数组中包含元素的有效区间为[front,rear-1]。
- 入队操作:将输入元素赋值给rear索引处,并将size+1 。
- 出队操作:只需将front+1 ,并将size-1 。
在不断进行入队和出队的过程中,front和rear都在向右移动,当它们到达数组尾部时就无法继续移动了。为了解决此问题,我们可以将数组视为首尾相接的“环形数组”。
对于环形数组,我们需要让front或rear在越过数组尾部时,直接回到数组头部继续遍历。这种周期性规律可以通过“取余操作”来实现,代码如下所示:
typedef struct {
int *nums;//用于存储队列元素的数组
int front;//队首指针,指向队首元素
int queSize;//尾指针,指向队尾 + 1
int queCapacity;//队列容量
} ArrayQueue;
//队列初始化
ArrayQueue *QueueInit(int capacity) {
ArrayQueue *queue = (ArrayQueue *)malloc(sizeof(ArrayQueue));
// 化数组
queue->queCapacity = capacity;
queue->nums = (int *)malloc(sizeof(int) * queue->queCapacity);
queue->front = queue->queSize = 0;
return queue;
}
//销毁队列
void QueueDestroy(ArrayQueue *queue) {
free(queue->nums);
free(queue);
}
//判断队列是否为空
bool QueueEmpty(ArrayQueue *queue) {
return queue->queSize == 0;
}
//访问队首元素
int QueueTop(ArrayQueue *queue) {
assert(size(queue) != 0);
return queue->nums[queue->front];
}
//入队
void QueuePush(ArrayQueue *queue, int num) {
if (size(queue) == capacity(queue)) {
printf("队列已满\r\n");
return;
}
//计算队尾指针,指向队尾索引+1
//通过取余操作实现rear越过数组尾部后回到头部
int rear = (queue->front + queue->queSize) % queue->queCapacity;
//将num添加至队尾
queue->nums[rear] = num;
queue->queSize++;
}
//出队
int QueuePop(ArrayQueue *queue) {
int num = peek(queue);
//队首指针向后移动一位,若越过尾部,则返回到数组头部
queue->front = (queue->front + 1) % queue->queCapacity;
queue->queSize--;
return num;
}
以上实现的队列仍然具有局限性:其长度不可变。然而,这个问题不难解决,我们可以将数组替换为动态数组,从而引入扩容机制。
【总结】
队列也可以通过数组和链表的结构实现,但使用链表的结构实现更优一些,因为如果使用数组,出队列在数组头上出数据,效率比较低。
四、队列的应用
队列最实际的应用就是保持公平性的排队。第一个排队的就是1号,下一个人的号就是队尾的号+1。如果两个人同时来到窗口取号,就会产生一个竞争,这里涉及到操作系统中的原子操作和加锁概念等,也可以叫做生产者消费者模型。
除此之外,队列还可以用于广度优先遍历bfs,而深度优先遍历dfs常通过递归/栈来实现。
- 淘宝订单。购物者下单后,订单将加入队列中,系统随后会根据顺序处理队列中的订单。在双十一期间,短时间内会产生海量订单,高并发成为工程师们需要重点攻克的问题。
- 各类待办事项。任何需要实现“先来后到”功能的场景,例如打印机的任务队列、餐厅的出餐队列等,队列在这些场景中可以有效地维护处理顺序。