队列与双端队列:先进先出结构及其变体
如果说栈代表的是“最近进入的内容最先处理”,那么队列代表的就是另一种同样高频、但完全不同的秩序:先进先出。最早进入队列的元素,应该最先离开;后来进入的元素,则排在后面等待处理。这个规则在现实里非常自然,无论是排队买票、任务等待执行、网络请求缓冲,还是图的广度优先遍历,背后都能看到队列的影子。
和栈一样,队列也是一种对线性表访问方式做了主动限制的抽象数据类型。它并不允许你在中间任意插入、删除或随机访问,而是把操作明确分工到两端:一端负责进入,一端负责离开。也正是因为这种规则足够稳定,队列特别适合表达那些“到达顺序本身就是处理顺序”的问题。
但队列这一类结构并不只有最基础的 FIFO 队列。稍微把限制放宽一点,就会出现双端队列;如果再结合优先级,后面还会引出优先队列。也就是说,队列不是一个孤立的小知识点,而是一整类“围绕处理顺序组织数据”的结构起点。这一篇就先把最核心的两类讲清楚:普通队列,以及双端队列。
一、什么是队列
队列可以先用一句非常直观的话来理解:
队列是一种只允许在一端插入、在另一端删除的线性结构。
通常:
- 插入的一端叫队尾;
- 删除的一端叫队头。
于是它的核心规则就非常明确:
- 新元素只能从队尾进入;
- 元素离开时只能从队头出去;
- 因此最先进入队列的元素会最先被处理。
这就是先进先出,也就是 First In First Out,常写作 FIFO。
可以把队列想成排队过闸机:
- 新来的人只能站到队尾;
- 真正被放行的人一定是队头那个;
- 中间的人不能随便跳出来先走。
这类顺序非常适合描述“谁先到,谁先处理”的系统行为。
二、队列通常支持哪些操作
从 ADT 的角度看,队列最常见的操作通常有这些:
enqueue(x):入队,把元素加入队尾;dequeue():出队,从队头移除元素;front():查看队头元素但不删除;isEmpty():判断队列是否为空;size():获取元素个数。
和栈一样,队列的接口也非常克制。它并不鼓励任意位置操作,而是让使用者只关心“进入顺序”和“离开顺序”。
这类接口限制看似保守,实际上非常有价值,因为它让顺序语义天然稳定下来。
三、队列为什么是线性表的另一种受限版本
如果拿队列和栈对比,会发现它们都属于“从线性表中抽象出来的受限结构”,只是限制方式不同。
1. 栈的限制
- 只在同一端插入和删除;
- 对应的是后进先出。
2. 队列的限制
- 一端插入,另一端删除;
- 对应的是先进先出。
也就是说,二者都不是自由访问的结构,而是通过限制操作方式,提炼出特定问题最需要的顺序模型。
这再次说明数据结构里一个很重要的思想:
限制操作并不一定是削弱能力,很多时候是在换取更清晰的语义和更贴切的问题表达。
四、为什么队列特别适合“到达即等待处理”的问题
因为队列的顺序规则,和很多真实系统里的处理逻辑高度一致。
例如:
- 任务调度里,先到的任务通常先处理;
- 打印任务会按进入打印缓冲区的顺序等待;
- 请求进入服务端时,很多场景会按到达顺序排队;
- 图的广度优先遍历里,先发现的节点先扩展下一层。
这些问题的共同点都是:
- 元素需要先暂存起来;
- 处理不是立刻发生;
- 谁先进入等待区,谁更早轮到。
这正是队列最擅长的模式。
五、顺序队列是什么
顺序队列可以理解成:
用顺序存储结构来实现队列,通常用数组配合两个位置变量来维护队头和队尾。
最直观的想法通常是:
- 用一个数组存元素;
front指向队头位置;rear指向队尾之后的位置,或者直接指向队尾。
入队时,把新元素放到队尾方向;
出队时,让队头向后移动。
这种实现思路本身没有问题,但如果只是朴素地一直后移,会很快遇到一个典型问题:假溢出。
六、什么是假溢出,为什么会出现循环队列
假设一个顺序队列用数组实现。
- 前面有元素不断出队;
- 队头位置往后移动;
- 后面再继续入队时,队尾也一直往后移。
这样一来,即使数组前半部分已经空出来了,如果实现不复用这些空间,队尾最终还是可能走到数组末端,表现得像“队列满了”。
可实际上:
- 整个数组并没有真的装满;
- 只是前面的空位没被重新利用。
这就是所谓的假溢出。
为了解决这个问题,就引出了循环队列。
循环队列的核心思路是:
把底层数组逻辑上首尾相连,当指针走到末端时,可以回到开头继续使用空闲位置。
这样就能更充分地利用顺序存储空间,也让顺序队列真正具备更实用的工程价值。
七、循环队列为什么这么重要
循环队列几乎是顺序队列里最值得重点掌握的版本。
因为它同时保留了两件事:
- 数组实现的简单和高效;
- 对空间的重复利用能力。
在循环队列中,通常会维护:
- 队头指针;
- 队尾指针;
- 有时还会额外维护当前元素数量。
每次入队或出队时,指针都按“模数组长度”的方式循环前进。
这类实现特别适合:
- 固定容量缓冲区;
- 生产者消费者模型中的环形缓存;
- 网络、日志、消息缓冲等连续流式处理场景。
所以循环队列虽然在教材里看起来像“顺序队列的一个优化”,但在真实系统里其实非常高频。
八、链式队列又是什么
链式队列通常是:
用链表来实现队列,并维护队头和队尾两个指针。
它的典型做法是:
- 队头负责出队;
- 队尾负责入队;
- 新节点总接到尾部;
- 出队时从头节点移除。
这种实现的好处是:
- 不需要连续空间;
- 不会像固定长度顺序队列那样轻易受到容量上限影响;
- 只要维护好头尾指针,入队和出队通常都能做到
O(1)。
但链式队列也会有链表常见的代价:
- 每个节点有额外指针开销;
- 内存局部性通常不如数组;
- 实现细节和边界维护比纯数组略复杂。
所以顺序队列和链式队列的差别,本质上仍然是在连续存储与链式存储之间做取舍。
九、队列的复杂度通常怎么理解
如果实现合理,队列的核心操作复杂度通常都比较稳定。
1. 入队:通常是 O(1)
无论是循环队列还是链式队列,通常都只需要改动队尾附近的状态。
2. 出队:通常是 O(1)
只需要移除队头元素并更新队头位置。
3. 查看队头:通常是 O(1)
因为队头位置本来就被维护着。
4. 判空:通常是 O(1)
因为可以通过指针关系或元素个数直接判断。
所以和栈一样,队列也是一种“规则收紧之后,核心操作复杂度很稳定”的结构。
十、为什么 BFS 几乎天然要用队列
这是队列最经典的算法应用之一。
图或树的广度优先遍历 BFS,强调的是:
- 先访问当前层;
- 再访问下一层;
- 同一层里先被发现的节点,要先展开它的邻居。
这件事和队列的先进先出高度一致:
- 新发现的节点先进入队尾等待;
- 最早进入等待区的节点先从队头出来扩展;
- 整个过程天然形成按层推进的顺序。
也就是说,BFS 之所以常和队列绑定,不只是因为大家约定俗成,而是因为其访问逻辑本质上就是一个先进先出的处理模型。
后面学图遍历时,你会越来越明显感受到这一点:
DFS的深度回退更像栈;BFS的层次推进更像队列。
十一、双端队列是什么
双端队列,通常记作 deque,可以理解成:
允许在两端都进行插入和删除操作的线性结构。
也就是说,相比普通队列只允许“尾进头出”,双端队列把两端都开放了出来。
于是它通常支持:
- 从队头插入;
- 从队尾插入;
- 从队头删除;
- 从队尾删除。
你可以把它看成是:
- 比普通队列更灵活;
- 比一般线性表仍然更有顺序约束。
双端队列并不是为了“功能越多越好”,而是为了更适合某些需要两侧维护窗口、区间或状态的场景。
十二、双端队列常用在哪里
双端队列的高频应用非常多,尤其在算法和系统实现中都很实用。
1. 滑动窗口问题
例如求滑动窗口最大值、最小值,经常会用到单调双端队列。
2. 两端都可能发生状态变化的问题
比如某些缓存淘汰、区间维护、双向调度场景。
3. 既想保留队列式顺序,又需要两端灵活操作
这类情况下,双端队列通常会比普通队列更适合。
所以双端队列可以理解成“围绕队列思想做出的一个实用扩展”。
十三、双端队列和栈、队列是什么关系
这三者其实很适合放在一起比较。
1. 栈
- 同一端进,同一端出;
- 对应后进先出。
2. 队列
- 一端进,另一端出;
- 对应先进先出。
3. 双端队列
- 两端都可以进,也都可以出;
- 规则比普通队列更灵活。
从这个角度看,双端队列有点像是介于“严格受限结构”和“更灵活结构”之间的一种平衡方案。
十四、什么时候应该想到用队列
真正学会队列,关键还是模式识别能力。
如果一个问题具有下面这些特征,往往就应该考虑队列:
- 元素按到达顺序等待处理;
- 先来的状态应该先被消费;
- 需要一边接收新元素,一边按顺序弹出旧元素;
- 需要按层、按波次、按轮次推进;
- 存在明显的缓冲区或排队区概念。
而如果问题还额外表现出:
- 两端都可能发生状态变化;
- 既要维护头部淘汰,也要维护尾部追加;
- 需要构造某种“单调窗口”或双向维护结构;
那就可以进一步考虑双端队列。
十五、学习队列时最容易踩的坑
1. 只记住 FIFO 定义,不会识别场景
这和学栈时的问题很像。定义记住了,但题目里不容易想到。
2. 顺序队列实现时忽略假溢出问题
这会导致数组前面明明有空位,却无法继续入队。
3. 循环队列判满判空条件写错
这是实现里最常见的 bug 来源之一。
4. 只看到普通队列,不知道双端队列的价值
很多窗口类问题,如果没有双端队列视角,会很难写出高效解法。
5. 把队列和优先队列混在一起
普通队列按进入顺序处理;优先队列则按优先级处理。它们的顺序依据并不一样。
十六、为什么这一篇对后面很重要
队列看起来定义简单,但它往后连接的内容很多。
后面你会不断遇到和队列有关的主题:
- 二叉树的层序遍历;
- 图的
BFS; - 生产者消费者模型;
- 环形缓冲区;
- 双端队列与单调队列;
- 优先队列与堆。
也就是说,队列并不是一个只会在基础章节里停留的结构。它和栈一样,既是具体容器,也是很多算法流程和系统调度模型的核心表达方式。
总结
队列和双端队列的重要性,不只是因为它们常见,而是因为它们把“顺序处理”和“等待消费”这类问题抽象得非常自然。真正值得先建立起来的,是这些核心认识:
- 队列是一种一端插入、另一端删除的线性结构,核心规律是先进先出;
- 它适合表达按到达顺序处理、按层推进和缓冲等待的问题;
- 顺序队列实现简单,但会遇到假溢出问题,因此循环队列非常重要;
- 链式队列不依赖连续空间,也能稳定支持
O(1)的入队和出队; - 双端队列允许在两端插入和删除,是对普通队列的重要扩展;
- 真正学会队列,不是只会
enqueue和dequeue,而是能识别“谁先进入等待区,谁就该先被处理”的问题模式。
把这一篇理解透之后,后面再去学字符串、树的层序遍历、图的 BFS 和优先队列时,你会越来越清楚地看到:很多看起来不同的问题,背后其实都在围绕“处理顺序”这件事展开。而队列,正是理解这类问题最基础的模型之一。
参考资源: