文章目录
数据结构6:队列
上一章我们了解并实现了栈这个数据结构,本章节开始了解队列和实现队列这个数据结构。
队列的概念及结构
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)
入队列 :进行插入操作的一端称为队尾
出队列 :进行删除操作的一端称为队头
栈和队列就是完全相反的数据结构,栈是先进后出,队列则是先进先出。

队列的实现
队列也可以数组和链表的结构实现,使用链表的结构实现普通队列会更优一些,因为如果使用数组的结构,出队列就是在做头删操作,并且在后续数据量上来了每一次扩容其实都有可能对空间进行浪费。

队列是先进先出的结构:只能从一端进,从另一端出。
之所以用链表结构来从当队列的子结构是:
- 普通队列链表插入和删除都是O(1)的,而数组只有尾插和尾删是O(1),其他的插入和删除都是O(N);
- 需要多少节点就 malloc 多少,不会像数组那样固定死或需要扩容。
- 数组队列出队后前面的空间浪费,链表不存在这个问题。
c
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int QDatatype;
typedef struct QueueNode
{
struct QueueNode* next;
QDatatype data;
}QNode;
typedef struct Queue
{
QNode* head;
QNode* tail;
int size;
}Queue;
结构设计介绍:
- head指针只管出队列,tail指针只管入队列,互不影响
- size是为了获取当前队列的有效数据个数,可加可不加

实现函数接口:
c
// 初始化队列
void QueueInit(Queue* pq);
// 销毁队列
void QueueDestroy(Queue* pq);
// 入队列
void QueuePush(Queue* pq, QDatatype x);
// 出队列
void QueuePop(Queue* pq);
// 获取队列有效数据个数
int QueueSize(Queue* pq);
// 判断队列是否为空
bool QueueEmpty(Queue* pq);
// 获取队头数据
QDatatype QueueFront(Queue* pq);
初始化队列
c
// 初始化队列
void QueueInit(Queue* pq)
{
pq->head = NULL;
pq->tail = NULL;
pq->size = 0;
}
空队列就是两个指针都为 NULL,表示链表里一个节点都没有。
队列判空
c
// 判断队列是否为空
bool QueueEmpty(Queue* pq)
{
assert(pq);
// 因为我们这个结构也可以用size进行判空
return pq->head == NULL;
}
为什么这里只判了head?
因为head == NULL 和 tail == NULL在正确的实现里永远同时成立:
空队列:两个都是 NULL。
非空队列:两个都非 NULL。
使用链表实现的好处这里是其中之一,链表实现不需要判满但需要判空,而数组都需要判断。
入队列
c
// 入队列
void QueuePush(Queue* pq, QDatatype x)
{
assert(pq);
QNode* node = (QNode*)malloc(sizeof(QNode));
if (node == NULL)
{
perror("malloc fail");
return ;
}
node->data = x;
node->next = NULL;
// 入队列有两种情况:
// 1. 空队列
if (QueueEmpty(pq))
{
pq->head = pq->tail = node;
}
// 2. 非空队列
else {
pq->tail->next = node;
pq->tail = node;
}
pq->size++;
}
新节点的 next 必须先置 NULL,它是新的尾节点。
空队列是特殊情况:因为此时 tail 是 NULL,不能写 q->tail ->next = node(空指针解引用会崩)。必须让 head和 tail 同时指向新节点。
非空时:先把旧尾的 next 接到新节点,再更新 tail 。两步顺序不能反
出队列
c
// 出队列
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
QNode* cur = pq->head;
pq->head = cur->next;
// 如果出队列后head为空了,此时tail同样要置空
if (pq->head == NULL)
{
pq->tail = NULL;
}
free(cur);
cur = NULL;
pq->size--;
}
注意:如果出队列时需要获取出队列的数据需要将数据先保存再free。
获取队列有效元素个数
c
// 获取队列有效数据个数
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
因为我们这里队列最开始声明了size变量所以实现了该函数接口,如果没声明可以不实现。
获取队头数据
c
// 获取队头数据
QDatatype QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->data;
}
这里需要对队列进行判空,如果不进行判空当队列是空队列时指向的是NULL,此时会导致未定义从而导致程序崩溃。
销毁队列
c
// 销毁队列
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur != NULL)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = pq->tail = NULL;
pq->size = 0;
}
最后记得要置空 head 和 tail,并将 size 置零
环形队列
拓展了解环形队列。实际中我们有时还会使用一种队列叫循环队列。如操作系统课程讲解生产者消费者模型时可以就会使用循环队列。环形队列可以使用数组实现,也可以使用循环链表实现。

环形队列和普通队列最大的区别就是环形队列的长度是固定的,那么环形队列的长度是固定的那岂不是在实用性上不如普通队列。
并不是环形队列它主要用于处理临时数据,比如程序运行的日志,缓冲区等。

判满和判空如果图方便的话可以向我们上面实现普通队列一样定义一个size,因为环形队列的长度是固定了那么size == 0 就是空队列,size == MAX_SIZE(MAX_SIZE是define定义的)就是满队列,所以环形队列的实现通常是数组,因为方便管理。
完