1. 队列的基本概念
1.1概念
- 队列(queue)是限定在⼀端插⼊另⼀端删除的线性表。因此,对队列来说,插⼊那⼀端叫做队尾,插⼊叫⼊队,删除那⼀端叫队头,删除叫做出队。队列中的数据元素遵循先进先出(FIFO,First In First Out)的特性。
- 队列的⼊队出队顺序就简单多了,⽆论如何操作,⼀种⼊队顺序只对应⼀种出队顺序。
- 队列在计算机系统中应用非常广泛,例如操作系统中的任务调度 、打印任务排队 、消息队列 、广度优先搜索(BFS) 等场景,都离不开队列这种先进先出的数据结构。
1.2 队列的基本操作
c
// 初始化队列
void QueueInit(LinkQueue* q);
// 销毁队列
void QueueDestroy(LinkQueue* q);
// 检测队列是否为空,空返回真,否则返回假
bool QueueEmpty(LinkQueue* q);
// 获取队列中有效元素个数
int QueueSize(LinkQueue* q);
// 获取队头元素
QDataType QueueFront(LinkQueue* q);
// 将x⼊队列
void EnQueue(LinkQueue* q, QDataType x);
// 出队并返回队头数据
QDataType DeQueue(LinkQueue* q);
说明:
QDataType是队列中存储的数据类型,通常用typedef定义为int等具体类型;LinkQueue是链式队列的结构体类型,下文会给出完整定义。
2. 队列的链式存储
2.1 链式队列的结构
-
队列的链式存储,我们可以选⽤单链表结构,也可以选⽤双向链表结构。他们⼊队对应着在表尾插⼊,出队对应着在表头删除。当然我们完全没必要选择双向链表,因为单链表就可以⾼效实现,还省空间⼀些。双向链表没有优势,每个结点还要多存储⼀个前驱指针,使⽤它纯粹浪费了。
-
其次这⾥我们选择单链表实现,⼊队就是尾插,出队就是头删。可以带头结点,也可以不带头结点,带头结点的结构略简单⼀点。
-
链式队列的核心结构如下:用
head指向队头结点,用tail指向队尾结点。入队时在tail之后插入新结点并更新tail,出队时删除head指向的结点并更新head。带头结点时,head指向哨兵位头结点,真正的队头元素是head->next。
c
// 链式队列的结点结构
typedef struct QNode {
QDataType data; // 数据域
struct QNode* next; // 指针域,指向下一个结点
} QNode;
// 链式队列的结构(带头结点)
typedef struct Queue {
QNode* head; // 指向哨兵位头结点
QNode* tail; // 指向队尾结点
} LinkQueue;
// 初始化队列:创建哨兵位头结点
void QueueInit(LinkQueue* q) {
q->head = (QNode*)malloc(sizeof(QNode));
if (q->head == NULL) {
printf("内存申请失败\n");
exit(-1);
}
q->head->next = NULL;
q->tail = q->head; // 空队列时,尾指针也指向哨兵位
}
// 销毁队列:释放所有结点
void QueueDestroy(LinkQueue* q) {
QNode* cur = q->head;
while (cur != NULL) {
QNode* next = cur->next;
free(cur);
cur = next;
}
q->head = q->tail = NULL;
}
// 检测队列是否为空:空返回真,否则返回假
bool QueueEmpty(LinkQueue* q) {
return q->head->next == NULL;
}
// 获取队列中有效元素个数
int QueueSize(LinkQueue* q) {
int size = 0;
QNode* cur = q->head->next;
while (cur != NULL) {
size++;
cur = cur->next;
}
return size;
}
// 获取队头元素(不删除)
QDataType QueueFront(LinkQueue* q) {
assert(q->head->next != NULL); // 队列非空
return q->head->next->data;
}
2.3 出队
c
// 入队:在队尾插入新结点
void EnQueue(LinkQueue* q, QDataType x) {
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL) {
printf("内存申请失败\n");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
q->tail->next = newnode; // 新结点链接到队尾
q->tail = newnode; // 更新尾指针
}
``````c
// 出队:删除队头结点并返回其数据
QDataType DeQueue(LinkQueue* q) {
assert(q != NULL);
assert(q->head->next != NULL); // 队列非空
QNode* del = q->head->next; // 待删除的队头结点
QDataType ret = del->data; // 保存数据
q->head->next = del->next; // 头结点指向下一个结点
if (q->tail == del) { // 删除的是最后一个结点
q->tail = q->head; // 尾指针回到哨兵位
}
free(del); // 释放结点
return ret;
}
注意:当队列中只剩一个结点时,删除后
tail必须重新指向哨兵位头结点,否则tail会变成野指针,后续入队操作将出错。
3. 队列的顺序存储
3.1 顺序队列的结构
-
顺序存储结构实现队列,⼊队对应尾插,出队对应头删,⽽顺序存储结构直接头删的需要挪动数据时间复杂度为 O(n),如果实现队列的顺序存储结构效率太低,就⽆价值了。所以这⾥可以调整思路,出队头删时,不再挪动数据,⽽是移动队头的下标或指针。
-
顺序队列的核心结构如下:用数组
a存储数据,用front指向队头下标,用rear指向队尾下标。入队时在rear处写入数据并让rear后移,出队时让front后移即可,这样出队的时间复杂度降为 O(1)。
c
// 顺序队列的结构
typedef struct SeqQueue {
QDataType* a; // 动态数组,存储数据
int front; // 队头下标
int rear; // 队尾下标
int capacity; // 数组容量
} SeqQueue;
// 初始化顺序队列
void SeqQueueInit(SeqQueue* q, int capacity) {
q->a = (QDataType*)malloc(sizeof(QDataType) * capacity);
if (q->a == NULL) {
printf("内存申请失败\n");
exit(-1);
}
q->front = 0;
q->rear = 0;
q->capacity = capacity;
}
// 入队:在队尾写入数据
void SeqQueuePush(SeqQueue* q, QDataType x) {
assert(q->rear < q->capacity); // 队列未满
q->a[q->rear] = x;
q->rear++;
}
// 出队:队头下标后移,不挪动数据
void SeqQueuePop(SeqQueue* q) {
assert(q->front < q->rear); // 队列非空
q->front++;
}
// 获取队头元素
QDataType SeqQueueFront(SeqQueue* q) {
assert(q->front < q->rear); // 队列非空
return q->a[q->front];
}
// 检测队列是否为空
bool SeqQueueEmpty(SeqQueue* q) {
return q->front == q->rear;
}
3.2 顺序队列的问题
-
顺序队列的⼀个经典问题就是假溢出问题,队列的存储空间(数组)中明明还有空闲位置,但由于队尾指针rear已经移动到了数组的末尾,导致⽆法再插⼊新元素,系统错误地判断为"队列已满"的现象,这种情况之所以被称为"假"溢出,是因为它并⾮真正的空间耗尽,⽽是⼀种由于数据结构设计缺陷导致的空间浪费。
-
例如:数组容量为 5,连续入队 5 个元素后
rear到达下标 5,此时再出队 3 个元素,front变为 3,数组下标 0~2 已经空闲,但rear仍指向末尾,无法继续入队------这就是假溢出。
c
// 演示假溢出问题
void DemoFakeOverflow() {
SeqQueue q;
SeqQueueInit(&q, 5);
// 连续入队 5 个元素,rear 到达末尾
for (int i = 1; i <= 5; i++) {
SeqQueuePush(&q, i);
}
// 出队 3 个元素,front 变为 3,下标 0~2 空闲
for (int i = 0; i < 3; i++) {
SeqQueuePop(&q);
}
// 此时数组前 3 个位置空闲,但 rear 已到末尾,无法入队
// SeqQueuePush(&q, 6); // 触发 assert 失败,即"假溢出"
}
解决思路 :采用循环队列 ,让
rear到达末尾后回绕到数组开头,从而复用空闲空间。循环队列中通常牺牲一个存储单元来区分队空与队满,即(rear + 1) % capacity == front表示队满,front == rear表示队空。