队列是和栈成对出现的受限线性数据结构 ,也是计算机系统中高频使用的基础结构。如果说栈负责"函数调用、逻辑回溯",那队列就负责排队、缓冲、异步、任务调度。
日常开发中的消息队列、线程池任务排队、网络IO缓冲、日志异步写入、服务器请求限流排队,底层全部依赖队列思想。本文从零吃透队列的底层原理、两种实现、循环队列优化、核心优缺点以及实战面试考点。
一、队列的核心定义与特性
1.1 什么是队列?
队列是一种**先进先出(FIFO,First In First Out)**的受限线性表。
通俗理解就是排队:先来的人先走,后来的人后走,绝对不允许插队、不允许从中间删除元素。
和栈刚好完全相反:
-
栈:后进先出(LIFO)
-
队列:先进先出(FIFO)
1.2 队列固定规则
-
队尾(rear):只能从队尾插入元素(入队)
-
队头(front):只能从队头删除元素(出队)
-
不支持中间插入、中间删除、随机访问
1.3 核心操作(全部 O(1))
-
enqueue 入队:队尾添加元素
-
dequeue 出队:队头删除元素
-
front 取队头:查看队首元素,不删除
-
empty 判断空队列:判断队列是否无数据
二、队列的两种底层实现
队列和栈一样,属于逻辑结构 ,物理底层依托两种基础结构:顺序队列(数组) 、链式队列(链表)。
2.1 顺序队列(普通数组实现)
利用连续数组存储数据,用两个标记变量控制头尾:
-
front:指向队头位置
-
rear:指向队尾下一个位置
存在致命问题:假溢出(伪溢出)
普通顺序队列最大的缺陷:数组前面出队空出来的空间永远无法复用。
当 rear 走到数组末尾,就算前面有大量空位,也会判定队列满,无法继续入队,这就是假溢出。
为了解决这个问题,工程中几乎不用普通顺序队列 ,全部使用优化后的循环队列。
2.2 循环队列(顺序队列终极优化)
循环队列是面试最高频考点,核心思想:让数组头尾相连,利用取模运算复用前面的空闲空间。
不再让 rear 一直往后走,而是通过 % capacity 实现环形循环。
循环队列核心公式
-
队尾更新:
rear = (rear + 1) % cap -
队头更新:
front = (front + 1) % cap
如何判断空、满?(面试必背)
循环队列最大难点:front 和 rear 重合时,分不清是空还是满。工程标准解决方案:预留一个空闲位置
-
队空 :
front == rear -
队满 :
(rear + 1) % cap == front
循环队列优点
-
彻底解决假溢出,100% 利用数组空间
-
无元素移动、效率极高、缓存友好
-
操作系统、缓冲区、环形缓冲区普遍使用
2.3 链式队列(链表实现)
基于单链表实现,为了保证 O(1) 效率,维护两个指针:
-
front 指针:指向链表头(出队位置)
-
rear 指针:指向链表尾(入队位置)
操作逻辑
-
入队:尾部新增节点,rear 后移,O(1)
-
出队:头部删除节点,front 后移,O(1)
链式队列优缺点
✅ 优点:无固定容量、无溢出问题、动态扩容、适合数据量波动大的场景
❌ 缺点:缓存不友好、存在指针开销、频繁 new/delete 性能损耗
三、顺序队列 vs 链式队列(工程选型)
| 特性 | 循环顺序队列 | 链式队列 |
|---|---|---|
| 内存结构 | 连续内存,缓存友好 | 离散内存,缓存较差 |
| 容量限制 | 固定容量 | 无上限,动态扩容 |
| 性能 | 极高,无内存碎片 | 略低,频繁分配释放 |
| 适用场景 | 缓冲区、固定大小任务队列 | 大量动态任务、不确定数据量 |
四、特殊队列(进阶高频)
4.1 双向队列 deque
普通队列只能一头进、一头出;双向队列两头都能进、两头都能出。
C++ STL deque 就是双向队列,支持:头尾插入、头尾删除,是栈和队列的结合体。
4.2 优先队列 priority_queue
不遵循先进先出,而是按照优先级出队。
底层默认基于大根堆实现,常用于:任务优先级调度、TOPK 问题、事件排序。
五、队列在计算机底层的真实应用
队列是系统工程中使用最多的数据结构,远超栈和链表。
5.1 操作系统任务调度
操作系统就绪队列、阻塞队列、时间片轮转调度,全部依靠队列管理进程/线程。
5.2 网络 IO 模型
epoll 就绪队列、内核 socket 读写缓冲区、TCP 滑动窗口,底层都是环形队列。
5.3 异步缓冲队列
日志异步队列、消息队列、线程池任务队列,用来削峰、解耦、异步处理。
5.4 限流与排队机制
服务器请求过多时,放入队列排队,防止瞬间打崩服务。
5.5 广度优先搜索 BFS
二叉树层序遍历、图 BFS 遍历,算法底层完全依赖队列。
六、线性表四大结构终极对比(面试必背)
| 结构 | 规则 | 访问方式 | 核心用途 |
|---|---|---|---|
| 顺序表 | 自由读写 | 随机访问 O(1) | 存储、查询多 |
| 链表 | 自由读写 | 顺序访问 O(n) | 频繁增删 |
| 栈 | 后进先出 | 仅栈顶访问 | 函数调用、回溯、括号匹配 |
| 队列 | 先进先出 | 仅头尾访问 | 排队、缓冲、任务调度、BFS |
普通顺序队列 代码实现(数组版)
底层基于静态数组实现,可直观体现假溢出问题,帮助理解队列底层缺陷。
cpp
#include <iostream>
using namespace std;
// 最大队列容量
const int MAX_SIZE = 5;
// 普通顺序队列
class ArrayQueue {
private:
int data[MAX_SIZE];
int front; // 队头:指向队首元素
int rear; // 队尾:指向队尾元素的下一个位置
public:
// 构造函数:初始化空队列
ArrayQueue() {
front = 0;
rear = 0;
}
// 判断队列是否为空
bool empty() {
return front == rear;
}
// 判断队列是否满
bool full() {
return rear == MAX_SIZE;
}
// 入队操作
bool enqueue(int val) {
if (full()) {
cout << "队列已满,入队失败" << endl;
return false;
}
data[rear++] = val;
return true;
}
// 出队操作
bool dequeue() {
if (empty()) {
cout << "队列为空,出队失败" << endl;
return false;
}
front++;
return true;
}
// 获取队头元素
int getFront() {
if (empty()) return -1;
return data[front];
}
// 遍历打印队列
void print() {
if (empty()) {
cout << "队列为空" << endl;
return;
}
cout << "队列元素:";
for (int i = front; i < rear; i++) {
cout << data[i] << " ";
}
cout << endl;
}
};
// 测试代码
int main() {
ArrayQueue q;
q.enqueue(1);
q.enqueue(2);
q.enqueue(3);
q.print();
q.dequeue();
q.print();
// 此处会触发假溢出:数组前方有空位,但rear到达末尾无法入队
q.enqueue(4);
q.enqueue(5);
q.enqueue(6); // 队列已满,入队失败
return 0;
}
核心缺陷:出队后数组前方产生空闲空间,但无法复用,出现假溢出,仅适合入门演示,工程不使用。
循环队列 代码实现(面试重点)
解决普通顺序队列假溢出问题,通过取模运算实现环形复用空间,采用业界标准:预留一个空位区分空/满状态,面试手撕最优版本。
cpp
#include <iostream>
using namespace std;
const int MAX_SIZE = 5;
class CircleQueue {
private:
int data[MAX_SIZE];
int front; // 队头指针
int rear; // 队尾指针
public:
CircleQueue() {
front = 0;
rear = 0;
}
// 判断空队列
bool empty() {
return front == rear;
}
// 判断队列满(预留一个空位)
bool full() {
return (rear + 1) % MAX_SIZE == front;
}
// 入队
bool enqueue(int val) {
if (full()) {
cout << "循环队列已满" << endl;
return false;
}
data[rear] = val;
rear = (rear + 1) % MAX_SIZE;
return true;
}
// 出队
bool dequeue() {
if (empty()) {
cout << "循环队列为空" << endl;
return false;
}
front = (front + 1) % MAX_SIZE;
return true;
}
// 获取队头元素
int getFront() {
if (empty()) return -1;
return data[front];
}
// 遍历打印
void print() {
if (empty()) {
cout << "循环队列为空" << endl;
return;
}
cout << "循环队列元素:";
int i = front;
while (i != rear) {
cout << data[i] << " ";
i = (i + 1) % MAX_SIZE;
}
cout << endl;
}
};
// 测试代码
int main() {
CircleQueue q;
q.enqueue(10);
q.enqueue(20);
q.enqueue(30);
q.print();
q.dequeue();
q.print();
// 环形复用空闲空间,解决假溢出
q.enqueue(40);
q.enqueue(50);
q.print();
return 0;
}
核心考点复盘:
-
队空:
front == rear -
队满:
(rear + 1) % MAX_SIZE == front -
头尾指针更新:通过取模实现环形循环
链式队列 代码实现(链表版)
基于单链表实现,无固定容量、无溢出问题,动态扩容,适合数据量不确定的场景,所有操作时间复杂度 O(1)。
cpp
#include <iostream>
using namespace std;
// 链表节点
struct QueueNode {
int val;
QueueNode* next;
QueueNode(int v) : val(v), next(nullptr) {}
};
// 链式队列
class LinkQueue {
private:
QueueNode* front; // 队头指针
QueueNode* rear; // 队尾指针
public:
// 初始化空队列
LinkQueue() {
front = nullptr;
rear = nullptr;
}
// 判断空队列
bool empty() {
return front == nullptr;
}
// 入队(尾部插入)
void enqueue(int val) {
QueueNode* newNode = new QueueNode(val);
// 空队列初始化头尾指针
if (empty()) {
front = newNode;
rear = newNode;
} else {
rear->next = newNode;
rear = newNode;
}
}
// 出队(头部删除)
bool dequeue() {
if (empty()) {
cout << "链式队列为空,出队失败" << endl;
return false;
}
// 暂存队头节点
QueueNode* temp = front;
front = front->next;
delete temp; // 释放内存,避免泄漏
// 如果删完后队列为空,重置尾指针
if (front == nullptr) {
rear = nullptr;
}
return true;
}
// 获取队头元素
int getFront() {
if (empty()) return -1;
return front->val;
}
// 遍历打印队列
void print() {
if (empty()) {
cout << "链式队列为空" << endl;
return;
}
cout << "链式队列元素:";
QueueNode* cur = front;
while (cur != nullptr) {
cout << cur->val << " ";
cur = cur->next;
}
cout << endl;
}
// 析构函数:释放所有节点内存
~LinkQueue() {
while (!empty()) {
dequeue();
}
}
};
// 测试代码
int main() {
LinkQueue q;
q.enqueue(100);
q.enqueue(200);
q.enqueue(300);
q.print();
q.dequeue();
q.print();
q.enqueue(400);
q.print();
return 0;
}
三种队列代码核心总结
-
普通顺序队列:代码最简单,存在假溢出,仅用于入门理解,不实战使用。
-
循环队列:面试必手撕,解决空间浪费,缓存友好,操作系统缓冲区、网络IO底层常用。
-
链式队列:动态扩容无上限,无需处理取模运算,适合任务量波动大的场景,需手动释放内存。
常见面试手撕问题
-
为什么循环队列要预留一个空位? 答:为了区分队空和队满状态,避免
front==rear状态歧义。 -
链式队列为什么维护头尾指针? 答:尾指针避免每次入队遍历链表尾部,保证入队操作 O(1)。
-
顺序队列和链式队列怎么选型? 答:固定数据量、追求高性能选循环顺序队列;数据量未知、动态增减选链式队列。
七、全文总结
-
队列是**先进先出(FIFO)**的受限线性表,队尾入队、队头出队,所有操作均为 O(1)。
-
普通顺序队列存在假溢出 问题,工程中统一使用循环队列解决空间浪费问题。
-
循环队列判空:
front == rear;判满:(rear+1)%cap == front,通过预留空位解决歧义。 -
链式队列基于链表实现,无容量限制,适合动态数据场景;顺序循环队列缓存友好、性能更高。
-
队列核心应用:操作系统调度、网络缓冲区、异步队列、BFS 算法、消息中间件、线程池任务排队。