如果说栈是在强调:
最近压进去的那个,最先处理
那队列强调的就是一种完全相反的秩序:
先进来的那个,先处理。
这就是队列最核心的规则:FIFO,先进先出。
也正因为这个规则特别接近现实世界里的"排队逻辑",
队列会让人有一种天然的熟悉感:
- 食堂排队
- 打印任务等待
- 消息一个个处理
- 图的广度优先搜索
- 二叉树的层序遍历
这些问题里,你都能看到队列的影子。
仓库位置:
D:\Users\30335\c++技术栈学习\data-structure\队列
核心文件:
Queue.hQueue.ctest.c
一、队列到底是什么
队列也是一种线性表,
但它不像顺序表那样支持任意位置操作,也不像栈那样只在一端进出。
它的规则很明确:
- 队尾入队
- 队头出队
所以它有两个端点:
- 队头
front - 队尾
rear/tail
这个规则听起来很普通,但它带来的是一种特别稳定的"流程顺序"。
也就是说,队列最适合的,不是"回退型问题",而是"推进型问题"。
二、为什么队列通常更适合链表实现
仓库里队列的核心结构是这样:
c
typedef struct QueueNode
{
QDataType val;
struct QueueNode* next;
} QNode;
typedef struct Queue
{
QNode* phead;
QNode* ptail;
int size;
} Queue;
这说明这个队列采用的是链式实现。
为什么链表很适合队列?
因为队列最常做的就两件事:
- 队尾插入
- 队头删除
如果你同时维护好头尾指针,那么这两个操作都可以很顺。
相比之下,如果用顺序表或普通数组直接做队列,
一旦频繁头删,就容易碰到前移数据的问题。
所以链式队列是很自然的工程选择。
三、头尾指针为什么缺一不可
你看这个结构:
c
QNode* phead;
QNode* ptail;
int size;
这三个成员各自负责的事很明确:
1. phead
永远指向当前队头。
出队时主要靠它。
2. ptail
永远指向当前队尾。
入队时主要靠它。
3. size
记录当前元素个数。
判空、取大小时会更方便。
很多初学者会问:
"队列一定要维护 size 吗?"
不一定,但维护了之后,很多操作会更直观,也少了一些重复计算。
四、队列初始化和销毁,表面简单,其实是在建立和清理结构边界
仓库里的初始化很标准:
c
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
这代表一个空队列的状态是:
- 头为空
- 尾为空
- 元素个数为 0
而销毁则是一路释放结点:
c
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* pcur = pq->phead;
while (pcur)
{
QNode* next = pcur->next;
free(pcur);
pcur = next;
}
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
这里最值得学的,其实不是 while 循环,
而是这种"最后把状态重新清零"的意识。
因为一个结构真正结束时,不只是内存放掉,
还要把外部可见状态恢复到"空"的样子。
五、入队为什么天然适合单链表尾插
仓库里的入队是这样实现的:
c
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(1);
}
newnode->next = NULL;
newnode->val = x;
if (pq->ptail == NULL)
{
pq->phead = pq->ptail = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++;
}
这里有一个很值得记住的小结构:
空队列时
新结点既是头,也是尾。
非空队列时
- 原队尾的
next指向新结点 - 队尾指针后移到新结点
这就是典型的链式尾插逻辑。
也正因为队列的入队天然发生在尾部,
所以链表实现显得特别顺。
六、出队为什么最容易在边界上出问题
仓库里的出队实现如下:
c
void QueuePop(Queue* pq)
{
assert(pq && 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--;
}
为什么这里要专门区分"只有一个结点"和"一般情况"?
因为当队列只有一个结点时:
- 删掉它之后
phead要变空ptail也必须同步变空
如果你只改头,不改尾,
那尾指针就会悬空,后面再入队就很危险。
所以队列这章最容易翻车的地方,往往不是大思路错了,
而是"只剩最后一个结点时"没处理好。
七、为什么 QueueFront 和 QueueBack 这两个接口都很自然
队列和栈不一样。
栈通常只强调"看栈顶",
队列则天生有两个非常自然的观察点:
- 看队头
- 看队尾
所以这两个接口存在感都很强:
c
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
它们的意义不是"多提供两个小工具",
而是在明确告诉你:
队列这类结构,本质上就是围绕"头和尾"来运转的。
八、为什么层序遍历和广度优先搜索总爱用队列
这是队列最典型的应用场景之一。
以二叉树层序遍历为例,你会发现它特别像这样一种流程:
- 先把当前层的结点按顺序放进去
- 再按进入顺序一个个取出来处理
- 处理的时候,把下一层结点继续接到后面
这和队列的先进先出完全一致。
所以:
- 二叉树层序遍历
- 图的广度优先搜索
本质上都很像"当前任务处理完,再按顺序推进下一批任务"。
而这种"推进"感,正是队列最擅长表达的。
九、队列和栈为什么总要放在一起学
因为这两个结构看起来都很基础,
但它们背后的行为模型正好相反。
栈
- 后进先出
- 适合处理最近状态
- 更像回退结构
队列
- 先进先出
- 适合处理流程推进
- 更像调度结构
这也是为什么你学会它们之后,会开始慢慢建立一种更高层次的判断:
当前问题到底更像"回退",还是更像"推进"?
这个判断,比死记哪题用哪结构更重要。
十、队列这章真正该学到什么
队列看起来不复杂,但它很适合帮你建立"流程顺序"的结构意识。
当你开始看到下面这些场景时,第一反应会变得更自然:
- 谁先进来,谁先处理
- 当前层处理完,再处理下一层
- 当前任务做完,再按顺序调度后续任务
这时你就会知道,队列这章不是在教你"另一个简单容器",
而是在教你一种非常普遍的程序组织方式。
复习时只看这几句就够了
- 队列是先进先出结构
- 队头出队,队尾入队
- 链式结构非常适合实现队列
- 队列通常要维护头指针、尾指针和元素个数
- 最容易出错的是删除最后一个结点后的边界处理
- 层序遍历和广度优先搜索都特别适合用队列