赞
踩
上一节中已经向大家介绍了栈的两种实现方法,今天我们来学习另一种线性存储结构--队列。先赞后看是好习惯!!!
目录
队列:只允许在一端插入数据,在另一端删除数据的特殊线性表,队列具有先进先出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);
- 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");
- 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++;
- }
- 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--;
- }
- QDataType QueueFront(Queue* pq)
- {
- assert(pq);
- // 暴力检查
- assert(pq->phead != NULL);
- return pq->phead->val;
- }
- QDataType QueueBack(Queue* pq)
- {
- assert(pq);
- // 暴力检查
- assert(pq->ptail != NULL);
- return pq->ptail->val;
- }
- bool QueueEmpty(Queue* pq)
- {
- assert(pq);
- return pq->size == 0;
- }
- 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常通过递归/栈来实现。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。