队列(Queue)
队列像食堂排队打饭:先来的人先打到饭走人,后来的人只能站队尾,先进先出(FIFO),绝对公平。
核心思想
队列是受限的线性表:一端(队尾)只能进(入队 Push),另一端(队头)只能出(出队 Pop),中间不能动。和栈相反,栈是 LIFO,队列是 FIFO。
我用带头尾指针的链式队列实现(也可以用数组):
phead:指向队头节点(出队端)。ptail:指向队尾节点(入队端)。size:节点数量。
关键设计:用 size 维护长度。如果没有 size,求队列长度要遍历 O(n);有了 size,入队出队时顺手 ++/--,判空和求长度都 O(1)。另外头尾双指针让入队(尾插)和出队(头删)都 O(1)——如果只有头指针,尾插要遍历找尾就 O(n) 了。
数据结构定义
typedef struct QueueNode // 链队节点
{
QDatatype data;
struct QueueNode *next;
}QNode;
typedef struct Queue // 队列管理结构:头尾指针 + size
{
QNode *phead; // 队头
QNode *ptail; // 队尾
int size; // 节点数
}Que;
内存模型(入队了 1,2,3,出队了 1 次,剩 2,3):
phead ptail
↓ ↓
[2|next] -> [3|next] -> NULL
队头 队尾
出队从 phead 删 入队在 ptail 后插
size = 2
两段式设计:QNode 是链表节点,Que 是管理结构(包住头尾指针和 size)。外部只持有 Que,不直接碰 QNode,封装性好。
关键操作实现
初始化 QueInit
void QueInit(Que *pq)
{
assert(pq);
pq->size = 0;
pq->phead = pq->ptail = NULL; // 头尾同时置空
}
逐行解释:空队列没有节点,头尾指针都置 NULL。必须同时置空:如果只置 phead 不置 ptail,ptail 残留野指针,第一次入队判 ptail==NULL 会误判。
入队 QuePush
void QuePush(Que *pq, QDatatype x)
{
assert(pq);
QNode *newnode = (QNode *)malloc(sizeof(QNode));
if (newnode == NULL) // malloc 检查
{
perror("malloc fail");
return;
}
newnode->next = NULL;
newnode->data = x;
if (pq->ptail == NULL) // 空队列
{
pq->phead = pq->ptail = newnode; // 头尾都指向新节点
}
else // 非空,尾插
{
pq->ptail->next = newnode; // 原尾的 next 指向新节点
pq->ptail = newnode; // 更新尾指针
}
pq->size++;
}
逐行解释:入队 = 尾插。空队列时新节点既是队头也是队尾,必须同时更新 phead 和 ptail(只更新一个会丢指针)。非空时让原尾节点的 next 指向新节点,再更新 ptail 指向新尾。size++ 维护长度。
陷阱:空队列入队只更新 ptail 忘更新 phead,导致 phead 仍为 NULL,出队时 phead->data 段错误。
出队 QuePop
void QuePop(Que *pq)
{
assert(pq);
assert(pq->size != 0); // 空队列不能出
if (pq->phead->next == NULL) // 只剩一个节点
{
free(pq->phead);
pq->phead = pq->ptail = NULL; // 头尾都置空,变空队列
}
else
{
QNode *next = pq->phead->next; // 先存第二个节点
free(pq->phead); // 再删队头
pq->phead = next; // 头指针后移
}
pq->size--;
}
逐行解释:出队 = 头删。分两种情况:
- 只剩一个节点:删完后队列变空,必须把
ptail也置 NULL,否则ptail变悬空指针,下次入队ptail->next段错误。 - 多个节点:用
next先存住原队头的下一个(防 free 后丢失),再free队头,头指针后移。
陷阱(最高频):只剩一个节点出队时,忘置空 ptail → 悬空指针。这是链式队列最经典的 bug。
取队头 QueFront / 取队尾 QueBack
QDatatype QueFront(Que *pq)
{
assert(pq);
assert(pq->phead); // 队列不能空
return pq->phead->data;
}
QDatatype QueBack(Que *pq)
{
assert(pq);
assert(pq->ptail); // 队列不能空
return pq->ptail->data;
}
逐行解释:双指针的红利——队头队尾都 O(1) 可取。assert 防止空队列取数据。注意 QueBack 是链式队列的优势,数组循环队列取队尾稍麻烦。
判空 QueEmpty
bool QueEmpty(Que *pq)
{
assert(pq);
return pq->size == 0; // 用 size 判空,O(1)
}
逐行解释:用 size 判空,也可以用 phead == NULL,等价。size 维护后判空和求长都 O(1)。
求长度 Quesize
int Quesize(Que *pq)
{
assert(pq);
return pq->size; // 直接返回,O(1)
}
逐行解释:size 的价值——不用遍历。命名问题:这里叫 Quesize(小写 s),但项目其他函数是 QuePush/QuePop(大写 P),命名风格不统一,应改成 QueSize。
销毁 QueDestroy(已修复)
void QueDestroy(Que *pq)
{
assert(pq);
QNode *cur = pq->phead;
while (cur)
{
QNode *next = cur->next;
free(cur); // 先释放当前节点,再移动到下一个,否则会内存泄漏
cur = next;
}
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
逐行解释:逐个 free 节点。必须先存 next 再 free(cur),否则 free 后 cur->next 读不到(UAF)。
修复前的 bug:原来写成 cur = NULL; cur = next;——先 cur = NULL 再 cur = next,等于什么都没释放(free 调用根本没发生),所有节点内存泄漏。正确是 free(cur) 再 cur = next。这是「想置空却忘了 free」的典型错误。
复杂度分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 入队 Push | O(1) | 尾插,ptail 直接定位 |
| 出队 Pop | O(1) | 头删,phead 直接定位 |
| 取队头 / 队尾 | O(1) | 双指针 |
| 判空 / 求长度 | O(1) | 维护 size |
| 销毁 | O(n) | 逐个释放 |
对比数组队列:数组队列出队要搬移所有元素 O(n)(除非用循环队列);链式队列头删 O(1),所以队列更适合用链表实现(栈更适合用数组,因为栈只在顶操作,数组天然合适)。
常见陷阱清单
- 只剩一个节点出队不置空
ptail→ 悬空指针,下次入队段错误(最高频)。 - 空队列判
ptail==NULL入队时只更新一个指针 → 丢队头或队尾。 - 销毁只置空不
free(修复前的 bug)→ 内存泄漏。 - 销毁先
free后取next→ UAF。 #include <queue.h>用尖括号:自定义头文件应用双引号"queue.h",尖括号是给系统头文件的。- 命名不统一:
Quesize应改QueSize。
我的实现 vs 教科书实现
- 设计正确:头尾双指针 + size,入队出队取头尾全 O(1),结构标准。
- 已修复销毁泄漏:
free(cur)顺序正确。 - 残留问题:
test.c用#include <queue.h>尖括号(应用引号);Quesize命名不统一。 - 缺接口:没有从队头遍历打印的接口(测试代码直接在 main 里循环出队打印)。
- vs 循环队列:链式队列无限扩容但每节点有指针开销;循环队列用定长数组 + 取模,省空间但要处理判空判满。
拓展知识点
变体与进阶
- 循环队列(Circular Queue):用定长数组实现,
front/rear指针取模复用空间。难点是判空判满——空是front==rear,满也是front==rear,冲突!两种解法:①牺牲一个单元(rear+1==front为满);②额外用size计数(本项目链队的思路)。 - 双端队列 Deque:两端都能入队出队,
std::deque底层是分块数组,支持 O(1) 头尾操作 + 随机访问。 - 优先队列(Priority Queue):出队按优先级而非时间,底层用堆(见 Binary_Tree 模块),O(log n)。
- 阻塞队列:线程间通信的基础,队空时取阻塞、队满时入阻塞。
常见考点(必刷)
- 用两个栈实现队列:入队压栈 A,出队时若栈 B 空,把 A 全倒进 B 再弹出。
- 循环队列判空判满:牺牲单元法 vs size 计数法。
- BFS 层序遍历:队列是 BFS 的核心数据结构(见 Binary_Tree 的
TreeLevelOrder)。 - 滑动窗口最大值:单调队列,O(n)。
与其他结构的关系
队列是 BFS、层序遍历、任务调度的基础。本项目 Binary_Tree 模块复用了一个 queue(存 Treenode*)来做层序遍历和完全二叉树判断——这是「数据结构组合使用」的典型。队列和栈是受限线性表的两兄弟,可互相模拟。
一句话记忆法
队列:先进先出(FIFO),尾入头出;头尾双指针 + size 让操作全 O(1),单节点出队别忘置空 ptail,销毁先存 next 再 free。
#DataStructure #CS #Queue #FIFO #受限线性表



被折叠的 条评论
为什么被折叠?



