
个人主页:
wengqidaifeng
✨ 永远在路上,永远向前走
文章目录
- 前言:从排队哲学到计算机科学
-
- 一、队列的起源:人类社会的朴素智慧
- 二、从社会生活到计算机科学:队列的跨越
- 三、队列的意义:为什么它如此重要?
-
- [3.1 公平性的数学化表达](#3.1 公平性的数学化表达)
- [3.2 解耦生产者与消费者](#3.2 解耦生产者与消费者)
- [3.3 缓冲与削峰填谷](#3.3 缓冲与削峰填谷)
- 四、队列的思想图谱:同一本质的不同面孔
- 五、学习的意义:为什么要学队列?
-
- [5.1 从"用"到"造"的跨越](#5.1 从“用”到“造”的跨越)
- [5.2 数据结构思维的训练场](#5.2 数据结构思维的训练场)
- [5.3 复杂系统的认知起点](#5.3 复杂系统的认知起点)
- 六、本篇文章的旅程
- [📌 队列:先进先出的数据秩序](#📌 队列:先进先出的数据秩序)
-
- 一、队列的概念与结构
-
- [1.1 什么是队列?](#1.1 什么是队列?)
- [1.2 队列与栈的对比](#1.2 队列与栈的对比)
- [1.3 队列的常见形式](#1.3 队列的常见形式)
- 二、顺序队列(数组实现)
-
- [2.1 结构定义](#2.1 结构定义)
- [2.2 初始化](#2.2 初始化)
- [2.3 判空与判满](#2.3 判空与判满)
- [2.4 入队](#2.4 入队)
- [2.5 出队](#2.5 出队)
- [2.6 取队头/队尾](#2.6 取队头/队尾)
- [2.7 销毁](#2.7 销毁)
- [⚠️ 顺序队列的致命缺陷:假溢出](#⚠️ 顺序队列的致命缺陷:假溢出)
- 三、循环队列(数组实现)
-
- [3.1 设计思想](#3.1 设计思想)
- [3.2 结构定义](#3.2 结构定义)
- [3.3 关键问题:如何区分空和满?](#3.3 关键问题:如何区分空和满?)
- [3.4 初始化](#3.4 初始化)
- [3.5 判空与判满](#3.5 判空与判满)
- [3.6 入队](#3.6 入队)
- [3.7 出队](#3.7 出队)
- [3.8 取队头/队尾](#3.8 取队头/队尾)
- [3.9 销毁](#3.9 销毁)
- 四、链式队列(链表实现)
-
- [4.1 结构定义](#4.1 结构定义)
- [4.2 初始化](#4.2 初始化)
- [4.3 申请节点](#4.3 申请节点)
- [4.4 入队(尾插)](#4.4 入队(尾插))
- [4.5 出队(头删)](#4.5 出队(头删))
- [4.6 取队头](#4.6 取队头)
- [4.7 判空与大小](#4.7 判空与大小)
- [4.8 销毁](#4.8 销毁)
- 五、三种队列实现对比
- 六、总结与思考
-
- [6.1 队列的核心启示](#6.1 队列的核心启示)
- [6.2 从队列到更广阔的天地](#6.2 从队列到更广阔的天地)
- [6.3 最后的思考](#6.3 最后的思考)
前言:从排队哲学到计算机科学
一、队列的起源:人类社会的朴素智慧
队列,可能是人类文明史上最早形成的社会契约之一。
想象公元前3000年的古埃及,工人们在搬运建造金字塔的石块时,自然而然地形成了一条传递石料的人链------最先传递的人,石块最先到达;最后加入的人,必须等待。这就是最原始的队列。
想象公元前的古希腊剧场,公民们排队入场观看悲剧表演------先到者优先获得最好的座位。这也是队列。
想象现代超市的收银台、银行的服务窗口、机场的安检通道------我们每天都在与队列打交道,以至于几乎忘记了它的存在。
队列是人类对"公平"与"秩序"最朴素、最直接的表达。 它告诉我们:先来后到,先到先得。
二、从社会生活到计算机科学:队列的跨越
当计算机科学家们在20世纪50年代设计第一批操作系统时,他们面临一个棘手的问题:
多个程序同时请求使用CPU,该先处理哪一个?
答案如此自然,仿佛从人类数千年的社会经验中直接生长出来------排成队列,先到先执行。
就这样,队列从人类社会走进了计算机世界,成为操作系统进程调度的核心机制。FCFS(先来先服务)调度算法,本质上就是对队列最直接的计算机实现。
这揭示了一个深刻的洞见:
计算机科学并非凭空创造算法,而是将人类处理问题的智慧形式化、抽象化、机械化。
队列不是被"发明"的,而是被"发现"的------它早已存在于人类社会的运行逻辑中,计算机科学家只是将它翻译成了机器能理解的语言。
三、队列的意义:为什么它如此重要?
3.1 公平性的数学化表达
队列是"公平"这一抽象概念在计算机中的具体实现。它用最简洁的数据结构,表达了最深刻的分配伦理:
先到达的请求 → 先获得服务
后到达的请求 → 后获得服务
这种**FIFO(First In First Out)**特性,使队列成为资源分配场景的首选结构。
3.2 解耦生产者与消费者
队列的伟大之处在于:它让数据的生产者和消费者不必直接见面。
- 生产者只需将数据放入队列,无需知道谁将处理它、何时处理
- 消费者只需从队列取出数据,无需知道谁产生了它、何时产生
这种解耦 能力,使队列成为消息队列、任务队列、线程池等高级并发模型的基石。没有队列,现代分布式系统将寸步难行。
3.3 缓冲与削峰填谷
在现实世界中,生产速度和消费速度往往不匹配。
- 网络数据包的到达是突发性的,但CPU的处理是匀速的
- 用户请求的发起是随机的,但服务器的处理能力是固定的
队列作为缓冲区,像水库一样调节着数据流动的"洪峰"与"枯水期",保障系统稳定运行。这是队列另一个不可替代的价值。
四、队列的思想图谱:同一本质的不同面孔
有趣的是,队列的思想在不同领域有着不同的名字,但本质完全相同:
| 领域 | 名称 | 相同的本质 |
|---|---|---|
| 数据结构 | 队列 | FIFO线性表 |
| 操作系统 | 消息队列 | 进程间通信 |
| 网络 | 缓冲区 | 数据包暂存 |
| 硬件 | FIFO | 芯片内部数据缓冲 |
| 数据库 | 事务日志 | 顺序写入、顺序回放 |
| 日常生活 | 排队 | 先到先服务 |
这种跨领域的一致性,正是队列作为基础思想的证明。 掌握了队列,你就掌握了一种跨越软硬件、贯穿理论实践的通用思维工具。
五、学习的意义:为什么要学队列?
5.1 从"用"到"造"的跨越
大多数程序员知道如何使用队列------调用queue.push()和queue.pop()。但只有真正亲手用C语言实现过队列的人,才真正理解:
- 为什么顺序队列会有"假溢出"问题?
- 为什么循环队列能节省空间?
- 为什么链式队列没有长度限制?
- 指针操作中哪个顺序错了会导致内存泄漏?
从使用者到实现者,是程序员成长的必经之路。
5.2 数据结构思维的训练场
队列是抽象数据类型的完美教学案例:
- 逻辑结构:线性关系,先进先出
- 物理实现:可以是数组(顺序存储),也可以是链表(链式存储)
- 操作集:创建、销毁、入队、出队、判空、取队头
同一逻辑结构对应多种物理实现,这是计算机科学中抽象 与实现分离思想的最佳启蒙。
5.3 复杂系统的认知起点
消息队列是每秒处理百万级请求的中间件,线程池是现代并发编程的核心武器,网络协议栈是互联网赖以生存的基础设施。
但它们的本质,都指向一个我们即将亲手实现的、几十行代码的简单数据结构。
理解了队列,你就拿到了理解这些复杂系统的第一把钥匙。
六、本篇文章的旅程
在这篇博客中,我们将:
-
从零开始,用C语言实现两种队列:
- 🧱 顺序队列(数组实现)------感受空间的局限性
- ⚙️ 循环队列------解决假溢出的智慧
- 🔗 链式队列------突破容量限制的自由
-
深入每个函数,解释:
- 为什么这样写?
- 边界条件如何处理?
- 内存如何管理?
- 易错点在哪里?
-
对比分析,在不同场景下如何选择队列实现方式
-
窥见未来,队列如何演化成消息队列、任务调度系统
队列教会我们的,不仅是数据如何进出,更是秩序如何建立,资源如何分配,系统如何协作。
它可能是你学过的最简单的数据结构,却可能是你未来用得最频繁、影响最深远的一个。
让我们一起,从C语言开始,构建程序世界的秩序之美。
📌 队列:先进先出的数据秩序
一、队列的概念与结构
1.1 什么是队列?
队列(Queue) 是一种特殊的线性表,其特殊之处在于只允许在表的一端进行插入操作,在另一端进行删除操作。
- 队尾(Rear):允许插入的一端
- 队头(Front):允许删除的一端
这种操作约束形成了队列最核心的特性:先进先出(First In First Out, FIFO)。
入队 ← [ ][ ][ ][ ][ ] ← 出队
队尾 队头
1.2 队列与栈的对比
| 特性 | 队列 | 栈 |
|---|---|---|
| 原则 | FIFO (先进先出) | LIFO (后进先出) |
| 插入端 | 队尾 | 栈顶 |
| 删除端 | 队头 | 栈顶 |
| 类比 | 排队购物 | 叠盘子 |
一句话区分:队列是"先来后到",栈是"后来居上"。
1.3 队列的常见形式
根据底层实现的不同,队列主要有三种形式:
- 顺序队列:用数组实现,存在假溢出问题
- 循环队列:用数组实现,逻辑上成环,解决假溢出
- 链式队列:用链表实现,无容量上限
本文将逐一实现并讲解这三种队列。
二、顺序队列(数组实现)
2.1 结构定义
c
typedef int QDataType;
typedef struct SeqQueue {
QDataType* data; // 动态数组
int front; // 队头下标
int rear; // 队尾下标
int capacity; // 容量
} SeqQueue;
2.2 初始化
c
void SeqQueueInit(SeqQueue* pq, int capacity)
{
assert(pq);
pq->data = (QDataType*)malloc(sizeof(QDataType) * capacity);
if (pq->data == NULL) {
perror("malloc fail");
exit(1);
}
pq->front = 0;
pq->rear = 0; // 约定:rear指向下一个存放数据的位置
pq->capacity = capacity;
}
2.3 判空与判满
c
bool SeqQueueEmpty(SeqQueue* pq)
{
assert(pq);
return pq->front == pq->rear;
}
bool SeqQueueFull(SeqQueue* pq)
{
assert(pq);
return pq->rear == pq->capacity;
}
2.4 入队
c
void SeqQueuePush(SeqQueue* pq, QDataType x)
{
assert(pq);
if (SeqQueueFull(pq)) {
printf("队列已满,无法入队\n");
return;
}
pq->data[pq->rear] = x;
pq->rear++;
}
2.5 出队
c
void SeqQueuePop(SeqQueue* pq)
{
assert(pq);
assert(!SeqQueueEmpty(pq)); // 队列不能为空
pq->front++;
}
2.6 取队头/队尾
c
QDataType SeqQueueFront(SeqQueue* pq)
{
assert(pq);
assert(!SeqQueueEmpty(pq));
return pq->data[pq->front];
}
QDataType SeqQueueBack(SeqQueue* pq)
{
assert(pq);
assert(!SeqQueueEmpty(pq));
return pq->data[pq->rear - 1];
}
2.7 销毁
c
void SeqQueueDestroy(SeqQueue* pq)
{
assert(pq);
free(pq->data);
pq->data = NULL;
pq->front = pq->rear = pq->capacity = 0;
}
⚠️ 顺序队列的致命缺陷:假溢出
初始状态:front=0, rear=0
[ ][ ][ ][ ][ ]
入队a,b,c,d后:front=0, rear=4
[a][b][c][d][ ]
出队a,b后:front=2, rear=4
[ ][ ][c][d][ ] // front=2, rear=4
此时队列明明还有空间(下标0,1空闲),
但 rear == capacity,判满条件成立,无法继续入队!
这就是"假溢出"------数组前端空间浪费,后端却已满。
解决方案:循环队列
三、循环队列(数组实现)
3.1 设计思想
将数组逻辑上视为一个环:当rear到达数组末端时,如果数组前端还有空闲,就让rear回到0。
3.2 结构定义
c
typedef struct CircleQueue {
QDataType* data;
int front;
int rear;
int capacity; // 实际分配的容量
} CircleQueue;
3.3 关键问题:如何区分空和满?
循环队列中,front == rear 可能表示队列空 ,也可能表示队列满。
三种解决方案:
- 牺牲一个存储单元 :约定
(rear+1)%capacity == front为满 - 增设
size字段记录元素个数 - 增设
tag标志位
本文采用方案1(最经典)
3.4 初始化
c
void CircleQueueInit(CircleQueue* pq, int k)
{
assert(pq);
// 需要k个有效元素,实际分配k+1个空间
pq->data = (QDataType*)malloc(sizeof(QDataType) * (k + 1));
if (pq->data == NULL) {
perror("malloc fail");
exit(1);
}
pq->front = 0;
pq->rear = 0;
pq->capacity = k + 1; // 实际容量
}
3.5 判空与判满
c
bool CircleQueueEmpty(CircleQueue* pq)
{
assert(pq);
return pq->front == pq->rear;
}
bool CircleQueueFull(CircleQueue* pq)
{
assert(pq);
return (pq->rear + 1) % pq->capacity == pq->front;
}
3.6 入队
c
void CircleQueuePush(CircleQueue* pq, QDataType x)
{
assert(pq);
if (CircleQueueFull(pq)) {
printf("循环队列已满\n");
return;
}
pq->data[pq->rear] = x;
pq->rear = (pq->rear + 1) % pq->capacity; // 环状移动
}
3.7 出队
c
void CircleQueuePop(CircleQueue* pq)
{
assert(pq);
assert(!CircleQueueEmpty(pq));
pq->front = (pq->front + 1) % pq->capacity;
}
3.8 取队头/队尾
c
QDataType CircleQueueFront(CircleQueue* pq)
{
assert(pq);
assert(!CircleQueueEmpty(pq));
return pq->data[pq->front];
}
QDataType CircleQueueBack(CircleQueue* pq)
{
assert(pq);
assert(!CircleQueueEmpty(pq));
// rear指向的是下一个插入位置,队尾元素在rear-1(注意环状)
int index = (pq->rear - 1 + pq->capacity) % pq->capacity;
return pq->data[index];
}
3.9 销毁
c
void CircleQueueDestroy(CircleQueue* pq)
{
assert(pq);
free(pq->data);
pq->data = NULL;
pq->front = pq->rear = pq->capacity = 0;
}
✅ 循环队列完美解决了假溢出问题,空间利用率100%(除牺牲的一个单元)。
四、链式队列(链表实现)
4.1 结构定义
c
// 节点结构
typedef struct QueueNode {
QDataType data;
struct QueueNode* next;
} QueueNode;
// 队列结构(持有队头队尾指针)
typedef struct LinkedQueue {
QueueNode* front; // 队头指针
QueueNode* rear; // 队尾指针
int size; // 元素个数(可选)
} LinkedQueue;
4.2 初始化
c
void LinkedQueueInit(LinkedQueue* pq)
{
assert(pq);
pq->front = pq->rear = NULL;
pq->size = 0;
}
4.3 申请节点
c
QueueNode* BuyQueueNode(QDataType x)
{
QueueNode* node = (QueueNode*)malloc(sizeof(QueueNode));
if (node == NULL) {
perror("malloc fail");
exit(1);
}
node->data = x;
node->next = NULL;
return node;
}
4.4 入队(尾插)
c
void LinkedQueuePush(LinkedQueue* pq, QDataType x)
{
assert(pq);
QueueNode* newnode = BuyQueueNode(x);
if (pq->rear == NULL) { // 空队列
pq->front = pq->rear = newnode;
} else {
pq->rear->next = newnode; // 原队尾指向新节点
pq->rear = newnode; // 更新队尾指针
}
pq->size++;
}
4.5 出队(头删)
c
void LinkedQueuePop(LinkedQueue* pq)
{
assert(pq);
assert(pq->front != NULL); // 队列不能为空
QueueNode* del = pq->front;
pq->front = del->next; // 队头后移
// 如果删除后队列为空,需要同步修改rear
if (pq->front == NULL) {
pq->rear = NULL;
}
free(del);
del = NULL;
pq->size--;
}
4.6 取队头
c
QDataType LinkedQueueFront(LinkedQueue* pq)
{
assert(pq);
assert(pq->front != NULL);
return pq->front->data;
}
4.7 判空与大小
c
bool LinkedQueueEmpty(LinkedQueue* pq)
{
assert(pq);
return pq->front == NULL && pq->rear == NULL;
// 或 return pq->size == 0;
}
int LinkedQueueSize(LinkedQueue* pq)
{
assert(pq);
return pq->size;
}
4.8 销毁
c
void LinkedQueueDestroy(LinkedQueue* pq)
{
assert(pq);
QueueNode* pcur = pq->front;
while (pcur != NULL) {
QueueNode* next = pcur->next;
free(pcur);
pcur = next;
}
pq->front = pq->rear = NULL;
pq->size = 0;
}
✅ 链式队列无容量上限,入队出队均为O(1),空间按需分配。
五、三种队列实现对比
| 特性 | 顺序队列 | 循环队列 | 链式队列 |
|---|---|---|---|
| 底层结构 | 动态数组 | 动态数组 | 链表 |
| 空间利用 | 有假溢出,利用率低 | 利用率高(牺牲1单元) | 按需分配 |
| 容量限制 | 固定容量 | 固定容量 | 无上限(受内存限制) |
| 入队复杂度 | O(1) | O(1) | O(1) |
| 出队复杂度 | O(1) | O(1) | O(1) |
| 取队头 | O(1) | O(1) | O(1) |
| 内存开销 | 低(仅数组) | 低(仅数组) | 高(每个节点额外指针) |
| 适用场景 | 不推荐单独使用 | 固定容量、高频访问 | 容量不定、频繁增删 |
六、总结与思考
6.1 队列的核心启示
队列教会我们:
- 约束创造秩序------限制了操作位置,反而让数据的流动变得清晰可控
- 同一逻辑,多种实现------FIFO是抽象的规则,数组和链表是具体的载体
- 没有最好的,只有最适合的------循环队列适合固定容量,链式队列适合动态场景
- 边界条件决定成败------空队、满队、一个元素、销毁后...代码的健壮性在于对边界的敬畏
6.2 从队列到更广阔的天地
当你理解了队列:
- 加上优先级 ,就成了优先队列(堆)
- 加上双端 ,就成了双端队列(deque)
- 加上阻塞 ,就成了生产者-消费者模型的核心
- 加上网络 ,就成了消息队列(Kafka、RabbitMQ)
- 加上持久化 ,就成了任务队列(Celery)
队列是所有这一切的起点。
6.3 最后的思考
数据结构的学习,本质上是思维的训练。
单向链表教会我们"指向",双向链表教会我们"回溯",栈教会我们"后发先至",而队列教会我们"先来后到"。
这些看似简单的规则,构成了复杂数字世界的底层秩序。当你能够在代码中自如地指挥数据的进出流动,你已经在用计算机科学的方式理解这个世界了。
现在,动手实现你的第一个队列吧。 🚀