文章目录
一、队列的概念
队列(Queue):一种线性表数据结构,是一种只允许在表的一端进行插入操作,而在表的另一端进行删除操作的线性表。
顾名思义,队列模拟了排队现象,即新来的人不断加入队列尾部,而位于队列头部的人逐个离开。我们将队列头部称为 “队首”,尾部称为 “队尾”,将把元素加入队尾的操作称为 “入队”,删除队首元素的操作称为 “出队”
和栈不同的是,队列是一种 「先进先出(First In First Out)」 的线性表,简称为 「FIFO 结构」。
二、队列的类型
和栈类似类似,队列有也同样的两种存储表示方法:「顺序结构」 和 「链式结构」。
- 「顺序存储的队列」:利用一组地址连续的存储单元依次存放队列中从队头到队尾的元素,同时使用指针指向队头元素和队尾元素。
- 「链式存储的队列」:利用单链表的方式来实现队列。队列中元素按照插入顺序依次插入到链表的第一个节点之后,并使用队头指针指向链表头节点位置,也就是队头元素,尾指针指向链表尾部位置,也就是队尾元素。
这两种结构用哪个实现比较好呢?很明显,队列不适合数组的结构,因为出数据之后就要挪动数据,很麻烦,所以采用链式结构来实现。
而链式结构的队列一般选择链表的头作为队头,尾作为队尾。
三、队列的常用操作
-
入队(QueuePush): 将一个元素加入队尾。
-
出队(QueuePop): 移除并返回队头元素。
-
获取队头元素(QueueFront): 返回队头元素但不删除。
-
获取队尾元素(QueueBack): 返回队尾元素,通常用于确认最后加入的元素。
-
检查队列是否为空(QueueEmpty): 判断队列中是否还有元素。
-
获取队列大小(QueueSize): 统计队列中当前元素的个数(一般需要遍历整个链表,时间复杂度为 O(n))。
四、队列的实现
1. 结构体定义和接口函数声明(Queue.h)
//Queue.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h> // 之后会用到的头文件
typedef int QDataType;
typedef struct QueueNode {
struct QueueNode* next;
QDataType data;
}QueueNode;
typedef struct Queue {
QueueNode* head; // 头指针,负责出队
QueueNode* tail; // 尾指针,负责入队
}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);
// 获取队列的大小
int QueueSize(Queue* pq);
// 队列的判空
bool QueueEmpty(Queue* pq);
这里我们定义了两个指针。一个指向头,而一个指向尾,这便于我们入队的操作,这样就不用每次都遍历整个链表。
2. 接口函数的实现(Queue.c)
队列的初始化
QueueInit:初始化队列,将队列的
head
和tail
均置为NULL
,表示队列为空。
void QueueInit(Queue* pq) {
assert(pq); // 断言总是用好处的,可以防止别人用函数的时候用错
pq->head = NULL;
pq->tail = NULL;
}
队列的销毁
QueueDestroy:遍历队列,从队头开始依次释放每个节点,最后将
head
和tail
置为NULL
。
void QueueDestroy(Queue* pq){
QueueNode* cur = pq->head;
while (cur != NULL) {
QueueNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = pq->tail = NULL; // 注意置空
}
入队
QueuePush:首先分配一个新节点,将数据存入其中,并将
next
指针置为NULL
。如果队列为空,则更新head
和tail
均指向新节点。如果队列不为空,则将当前队尾的next
指向新节点,并更新tail
。注:分配新节点的操作可以再单独写一个函数来实现,这里我就没有单独写了。
void QueuePush(Queue* pq, QDataType x){
assert(pq);
QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode)); // 分配新节点
newNode->data = x; // 初始化新节点
newNode->next = NULL;
if (pq->head == NULL) { // 如果队列为空
pq->head = pq->tail = newNode;
}
else { // 不为空
pq->tail->next = newNode;
pq->tail = newNode;
}
}
出队
QueuePop:移除队头元素,首先检查队列不为空。保存队头节点的下一个节点指针,再来释放当前队头(注意顺序)。更新
head
指向下一个节点。
注意:如果队列中只有一个节点,释放后head
变为NULL
,此时应将tail
也置为NULL
,防止产生野指针。
void QueuePop(Queue* pq){
assert(pq);
assert(!QueueEmpty(pq)); // 确保队列非空
QueueNode* next = pq->head->next;
free(pq->head);
pq->head = next;
if (pq->head == NULL) { // 出队之后检查队列是否为空,为空则置空尾指针
pq->tail = NULL;
}
}
获取队头元素
QueueFront:获取队头元素的数据,但不删除。所以直接返回
head
指针指向的节点数据即可。
QDataType QueueFront(Queue* pq){
assert(pq);
assert(!QueueEmpty(pq)); // 确保队列非空
return pq->head->data;
}
获取队尾元素
QueueBack: 获取队尾元素的数据,不删除。 直接返回
tail
指针指向的节点数据即可。
QDataType QueueBack(Queue* pq){
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->data;
}
获取队列的大小
QueueSize:返回队列中元素的个数。从
head
开始遍历链表,计数直至链表尾部。
int QueueSize(Queue* pq){
assert(pq);
int n = 0;
QueueNode* cur = pq->head;
while (cur) {
++n;
cur = cur->next;
}
return n;
}
队列的判空
QueueEmpty: 检查队列是否为空,判断
head
是否为NULL
即可
bool QueueEmpty(Queue* pq){
assert(pq);
return pq->head == NULL;
}
完整代码
#include "Queue.h"
void QueueInit(Queue* pq) {
assert(pq);
pq->head = NULL;
pq->tail = NULL;
}
void QueueDestroy(Queue* pq){
QueueNode* cur = pq->head;
while (cur != NULL) {
QueueNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = pq->tail = NULL;
}
void QueuePush(Queue* pq, QDataType x){
assert(pq);
QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
newNode->data = x;
newNode->next = NULL;
if (pq->head == NULL) {
pq->head = pq->tail = newNode;
}
else {
pq->tail->next = newNode;
pq->tail = newNode;
}
}
void QueuePop(Queue* pq){
assert(pq);
assert(!QueueEmpty(pq));
QueueNode* next = pq->head->next;
free(pq->head);
pq->head = next;
//注意:当只有一个节点的时候,free 完了之后没有对 tail 进行处理就会变成野指针
if (pq->head == NULL) {
pq->tail = NULL;
}
}
QDataType QueueFront(Queue* pq){
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->data;
}
QDataType QueueBack(Queue* pq){
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->data;
}
int QueueSize(Queue* pq){
assert(pq);
int n = 0;
QueueNode* cur = pq->head;
while (cur) {
++n;
cur = cur->next;
}
return n;
}
bool QueueEmpty(Queue* pq){
assert(pq);
return pq->head == NULL;
}
3. 测试代码示例(test.c)
以下测试代码展示了如何使用上述接口对队列进行操作:
#include "Queue.h"
void TestQueue1() {
Queue q;
QueueInit(&q);
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
QueuePush(&q, 4);
printf("%d\n", QueueFront(&q));
printf("%d\n", QueueBack(&q));
while (!QueueEmpty(&q)) {
QDataType front = QueueFront(&q);
printf("%d ", front);
QueuePop(&q);
}
printf("\n");
}
int main() {
TestQueue1();
return 0;
}
运行结果如下:
六、结语
本文介绍了链式队列的基本原理与常用操作,同时详细解析了每个接口函数的设计与实现思路。希望这篇文章能帮助大家初步理解队列数据结构及其实现细节。