数据结构(三)栈和队列(下)队列:程序世界的秩序之美

个人主页:
wengqidaifeng

✨ 永远在路上,永远向前走

个人专栏:
数据结构
C语言
嵌入式小白启动!

文章目录


前言:从排队哲学到计算机科学

一、队列的起源:人类社会的朴素智慧

队列,可能是人类文明史上最早形成的社会契约之一。

想象公元前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 复杂系统的认知起点

消息队列是每秒处理百万级请求的中间件,线程池是现代并发编程的核心武器,网络协议栈是互联网赖以生存的基础设施。

但它们的本质,都指向一个我们即将亲手实现的、几十行代码的简单数据结构。

理解了队列,你就拿到了理解这些复杂系统的第一把钥匙。


六、本篇文章的旅程

在这篇博客中,我们将:

  1. 从零开始,用C语言实现两种队列:

    • 🧱 顺序队列(数组实现)------感受空间的局限性
    • ⚙️ 循环队列------解决假溢出的智慧
    • 🔗 链式队列------突破容量限制的自由
  2. 深入每个函数,解释:

    • 为什么这样写?
    • 边界条件如何处理?
    • 内存如何管理?
    • 易错点在哪里?
  3. 对比分析,在不同场景下如何选择队列实现方式

  4. 窥见未来,队列如何演化成消息队列、任务调度系统


队列教会我们的,不仅是数据如何进出,更是秩序如何建立,资源如何分配,系统如何协作。

它可能是你学过的最简单的数据结构,却可能是你未来用得最频繁、影响最深远的一个。

让我们一起,从C语言开始,构建程序世界的秩序之美。


📌 队列:先进先出的数据秩序

一、队列的概念与结构

1.1 什么是队列?

队列(Queue) 是一种特殊的线性表,其特殊之处在于只允许在表的一端进行插入操作,在另一端进行删除操作

  • 队尾(Rear):允许插入的一端
  • 队头(Front):允许删除的一端

这种操作约束形成了队列最核心的特性:先进先出(First In First Out, FIFO)

复制代码
    入队 ← [ ][ ][ ][ ][ ] ← 出队
         队尾          队头

1.2 队列与栈的对比

特性 队列
原则 FIFO (先进先出) LIFO (后进先出)
插入端 队尾 栈顶
删除端 队头 栈顶
类比 排队购物 叠盘子

一句话区分:队列是"先来后到",栈是"后来居上"。

1.3 队列的常见形式

根据底层实现的不同,队列主要有三种形式:

  1. 顺序队列:用数组实现,存在假溢出问题
  2. 循环队列:用数组实现,逻辑上成环,解决假溢出
  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 可能表示队列空 ,也可能表示队列满

三种解决方案:

  1. 牺牲一个存储单元 :约定(rear+1)%capacity == front为满
  2. 增设size字段记录元素个数
  3. 增设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 队列的核心启示

队列教会我们:

  1. 约束创造秩序------限制了操作位置,反而让数据的流动变得清晰可控
  2. 同一逻辑,多种实现------FIFO是抽象的规则,数组和链表是具体的载体
  3. 没有最好的,只有最适合的------循环队列适合固定容量,链式队列适合动态场景
  4. 边界条件决定成败------空队、满队、一个元素、销毁后...代码的健壮性在于对边界的敬畏

6.2 从队列到更广阔的天地

当你理解了队列:

  • 加上优先级 ,就成了优先队列(堆)
  • 加上双端 ,就成了双端队列(deque)
  • 加上阻塞 ,就成了生产者-消费者模型的核心
  • 加上网络 ,就成了消息队列(Kafka、RabbitMQ)
  • 加上持久化 ,就成了任务队列(Celery)

队列是所有这一切的起点。


6.3 最后的思考

数据结构的学习,本质上是思维的训练。

单向链表教会我们"指向",双向链表教会我们"回溯",栈教会我们"后发先至",而队列教会我们"先来后到"。

这些看似简单的规则,构成了复杂数字世界的底层秩序。当你能够在代码中自如地指挥数据的进出流动,你已经在用计算机科学的方式理解这个世界了。

现在,动手实现你的第一个队列吧。 🚀

相关推荐
程序员酥皮蛋1 小时前
hot 100 第二十三题 23.反转链表
数据结构·算法·leetcode·链表
TracyCoder1232 小时前
LeetCode Hot100(51/100)——155. 最小栈
数据结构·算法·leetcode
shentuyu木木木(森)3 小时前
单调队列 & 单调栈
数据结构·c++·算法·单调栈·单调队列
菜鸡儿齐3 小时前
leetcode-最长连续序列
数据结构·算法·leetcode
wengqidaifeng4 小时前
数据结构---链表的奇特(下)双向链表的多样魅力
c语言·数据结构·链表
im_AMBER4 小时前
Leetcode 118 从中序与后序遍历序列构造二叉树 | 二叉树的最大深度
数据结构·学习·算法·leetcode
cpp_25014 小时前
P10250 [GESP样题 六级] 下楼梯
数据结构·c++·算法·动态规划·题解·洛谷
蜕变的小白4 小时前
数据结构:排序算法与哈希表
数据结构·算法·哈希算法
程序员酥皮蛋5 小时前
hot 100 第二十二题 22.相交链表
数据结构·算法·leetcode·链表