3 队列
GESP C++ 六级:队列精讲
核心目标:理解队列的先进先出(FIFO)特性,掌握队列的数组模拟(含循环队列)与 STL
queue的使用,熟悉双端队列deque和优先队列priority_queue的基本操作,能运用队列解决 BFS、模拟、滑动窗口等经典问题。
队列是“两头开口”的容器——先进先出(FIFO);队尾入队,队头出队;数组模拟要处理假溢出,循环队列用取模;STL 用push/pop/front/back/empty/size;BFS、模拟、单调队列是三大高频应用。
一、考纲定位
根据 CCF 官方考纲,GESP 六级数据结构部分明确要求掌握栈和队列的基本概念与操作,以及它们在算法问题中的应用。
队列在六级考试中的考查分布:
| 考点 | 重要程度 | 典型考法 |
|---|---|---|
| 队列的基本概念(FIFO) | ★★★★★ | 选择题高频 |
| 队列的数组模拟实现 | 代码填空、程序阅读 | |
| 循环队列(取模) | 选择题、代码填空 | |
STL queue 的使用 |
★★★★☆ | 编程题 |
双端队列 deque |
编程题、选择题 | |
优先队列 priority_queue |
||
| BFS 广度优先搜索 | ★★★★★ | 编程题高频 |
| 滑动窗口最大值(单调队列) | ★★★★☆ | 编程题 |
| 队列与栈的区别 | ★★★★★ | 选择题 |
二、队列的基本概念
2.1 什么是队列
队列(Queue)是一种只能在一端插入、另一端删除的线性数据结构。允许插入的一端称为队尾(rear),允许删除的一端称为队头(front)。
队列的核心特性是先进先出(First In First Out,FIFO):最先入队的元素最先出队。
直观比喻:队列就像排队买票,先来的人先买到票,后来的人排在队尾。
2.2 队列的基本操作
| 操作 | 英文 | 含义 |
|---|---|---|
| 入队 | push / enqueue | 在队尾插入一个元素 |
| 出队 | pop / dequeue | 删除队头元素 |
| 取队头 | front | 读取队头元素(不删除) |
| 取队尾 | back | 读取队尾元素(不删除) |
| 判空 | empty | 判断队列是否为空 |
| 求大小 | size | 返回队列中元素个数 |
2.3 队列 vs 栈
| 对比项 | 队列 | 栈 |
|---|---|---|
| 操作端 | 队尾入队,队头出队 | 只在栈顶操作 |
| 特性 | 先进先出(FIFO) | 后进先出(LIFO) |
| 插入 | push(队尾) | push(栈顶) |
| 删除 | pop(队头) | pop(栈顶) |
| 典型应用 | BFS、任务调度、缓冲区 | 括号匹配、表达式求值、递归 |
三、队列的数组模拟实现
3.1 普通数组模拟(存在假溢出)
用数组 q[] 存储元素,front 指向队头,rear 指向队尾的下一个位置。
const int N = 100005;
int q[N];
int front = 0, rear = 0; // 初始为空
// 入队
void push(int x) {
q[rear++] = x;
}
// 出队
void pop() {
front++;
}
// 取队头
int getFront() {
return q[front];
}
// 判空
bool empty() {
return front == rear;
}
// 大小
int size() {
return rear - front;
}
问题:随着入队和出队,front 和 rear 不断增大,最终 rear 会超出数组范围,但数组前面可能还有空闲空间。这就是假溢出。
3.2 循环队列(解决假溢出)
循环队列通过取模运算让 front 和 rear 在数组范围内循环移动。
const int N = 100005;
int q[N];
int front = 0, rear = 0; // 初始为空
// 入队
bool push(int x) {
if ((rear + 1) % N == front) return false; // 队满
q[rear] = x;
rear = (rear + 1) % N;
return true;
}
// 出队
bool pop() {
if (front == rear) return false; // 队空
front = (front + 1) % N;
return true;
}
// 取队头
int getFront() {
return q[front];
}
// 判空
bool empty() {
return front == rear;
}
// 大小
int size() {
return (rear - front + N) % N;
}
关键点:
- 循环队列通常牺牲一个存储单元来区分队空和队满。
- 队空:
front == rear。 - 队满:
(rear + 1) % N == front。 - 入队:
q[rear] = x; rear = (rear + 1) % N; - 出队:
front = (front + 1) % N;
3.3 真题示例(代码填空)
循环队列中,判断队满的条件是( )。
A.front == rear
B.(rear + 1) % N == front
C.rear == N - 1
D.front == 0
答案:B
四、STL queue 的使用
C++ 标准库提供了 queue 容器,使用前需要包含头文件 <queue>。
4.1 基本用法
#include <queue>
using namespace std;
queue<int> q; // 创建一个存储 int 的队列
q.push(10); // 入队
q.push(20);
q.push(30);
cout << q.front() << endl; // 输出 10(队头)
cout << q.back() << endl; // 输出 30(队尾)
q.pop(); // 弹出队头
cout << q.front() << endl; // 输出 20
cout << q.size() << endl; // 输出 2
cout << q.empty() << endl; // 输出 0(false,不为空)
4.2 常用函数
| 函数 | 作用 | 示例 |
|---|---|---|
push(x) |
入队 | q.push(10); |
pop() |
出队(无返回值) | q.pop(); |
front() |
取队头 | int x = q.front(); |
back() |
取队尾 | int x = q.back(); |
empty() |
判空 | if (q.empty()) |
size() |
求大小 | int n = q.size(); |
重要注意:pop() 只删除队头元素,不返回被删除的值。如果需要获取队头值,必须先 front() 再 pop()。
int x = q.front(); // 先取
q.pop(); // 再删
4.3 使用前必须判空
if (!q.empty()) {
int x = q.front();
q.pop();
}
直接对空队列调用 front() 或 pop() 是未定义行为,可能导致程序崩溃。
五、双端队列 deque
deque(double-ended queue)是双端队列,允许在两端进行插入和删除操作。
5.1 基本用法
#include <deque>
using namespace std;
deque<int> dq;
dq.push_back(10); // 队尾入队
dq.push_front(20); // 队头入队
cout << dq.front() << endl; // 20
cout << dq.back() << endl; // 10
dq.pop_back(); // 队尾出队
dq.pop_front(); // 队头出队
5.2 常用函数
| 函数 | 作用 |
|---|---|
push_back(x) |
队尾插入 |
push_front(x) |
队头插入 |
pop_back() |
队尾删除 |
pop_front() |
队头删除 |
front() |
取队头 |
back() |
取队尾 |
empty() |
判空 |
size() |
求大小 |
5.3 与 queue 的区别
| 对比项 | queue |
deque |
|---|---|---|
| 插入端 | 只能队尾 | 队头、队尾都可以 |
| 删除端 | 只能队头 | |
| 访问队尾 | back() |
|
| 访问队头 | front() |
|
| 适用场景 | 普通队列 | 滑动窗口、双端操作 |
六、优先队列 priority_queue
priority_queue 是优先队列,每次出队的都是优先级最高的元素。默认是大根堆(最大值先出)。
6.1 基本用法
#include <queue>
using namespace std;
priority_queue<int> pq; // 默认大根堆
pq.push(3);
pq.push(1);
pq.push(4);
cout << pq.top() << endl; // 4(最大值)
pq.pop();
cout << pq.top() << endl; // 3
6.2 小根堆
priority_queue<int, vector<int>, greater<int>> pq; // 小根堆
6.3 常用函数
| 函数 | 作用 |
|---|---|
push(x) |
插入元素 |
pop() |
删除堆顶 |
top() |
取堆顶(优先级最高) |
empty() |
判空 |
size() |
求大小 |
6.4 应用场景
- 任务调度:每次处理优先级最高的任务。
- 求第 k 大/小元素。
- Dijkstra 最短路算法。
七、经典应用一:BFS 广度优先搜索
7.1 问题描述
给定一个迷宫(二维网格),求从起点到终点的最短步数。
7.2 解题思路
BFS 使用队列逐层扩展,保证第一次到达终点时步数最少。
#include <iostream>
#include <queue>
using namespace std;
struct Node {
int x, y, step;
};
int n, m;
char maze[105][105];
bool visited[105][105];
int dx[] = {-1, 1, 0, 0};
int dy[] = {0, 0, -1, 1};
int bfs(int sx, int sy, int ex, int ey) {
queue<Node> q;
q.push({sx, sy, 0});
visited[sx][sy] = true;
while (!q.empty()) {
Node cur = q.front();
q.pop();
if (cur.x == ex && cur.y == ey) {
return cur.step;
}
for (int i = 0; i < 4; i++) {
int nx = cur.x + dx[i];
int ny = cur.y + dy[i];
if (nx >= 0 && nx < n && ny >= 0 && ny < m &&
!visited[nx][ny] && maze[nx][ny] != '#') {
visited[nx][ny] = true;
q.push({nx, ny, cur.step + 1});
}
}
}
return -1; // 无法到达
}
7.3 复杂度
时间 O(n × m),空间 O(n × m)。
7.4 真题示例
用 BFS 求迷宫最短路径,队列中存储的是( )。
答案:当前节点的坐标和步数。
八、经典应用二:模拟排队
8.1 问题描述
模拟银行叫号系统:有 n 个顾客,每个顾客有到达时间和办理业务所需时间,求平均等待时间。
8.2 解题思路
用队列存储等待的顾客,按到达时间依次处理。
struct Customer {
int arrive, need;
};
queue<Customer> q;
int currentTime = 0;
int totalWait = 0;
while (!q.empty() || 还有顾客未到达) {
// 将已到达的顾客加入队列
while (还有顾客未到达 && 顾客到达时间 <= currentTime) {
q.push(该顾客);
}
if (q.empty()) {
currentTime = 下一个顾客的到达时间; // 空闲等待
continue;
}
Customer c = q.front();
q.pop();
totalWait += currentTime - c.arrive; // 等待时间
currentTime += c.need; // 办理业务
}
8.3 关键点
- 队列保证先到先服务。
- 处理空闲时间:如果队列为空,时间跳到下一个顾客到达时间。
九、经典应用三:滑动窗口最大值(单调队列)
9.1 问题描述
给定一个数组和窗口大小 k,求每个窗口中的最大值。
9.2 解题思路
用双端队列维护一个单调递减的队列,队头始终是当前窗口的最大值。
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> dq; // 存下标
vector<int> res;
for (int i = 0; i < nums.size(); i++) {
// 移除超出窗口的元素
while (!dq.empty() && dq.front() <= i - k) {
dq.pop_front();
}
// 维护单调递减
while (!dq.empty() && nums[dq.back()] <= nums[i]) {
dq.pop_back();
}
dq.push_back(i);
// 窗口形成后记录最大值
if (i >= k - 1) {
res.push_back(nums[dq.front()]);
}
}
return res;
}
9.3 复杂度
每个元素最多入队一次、出队一次,时间复杂度 O(n)。
9.4 关键点
- 队列中存下标,方便判断是否超出窗口。
- 队头是当前窗口最大值的下标。
- 保持队列单调递减:新元素入队前,弹出所有比它小的队尾元素。
十、GESP 六级队列真题精讲
真题1:队列的基本操作(选择题)
以下关于队列的说法,正确的是( )。
A. 队列是后进先出的数据结构
B. 队列只能在队头进行插入和删除
C. 队列的插入操作在队尾进行,删除操作在队头进行
D. 队列可以随机访问任意元素
答案:C
真题2:循环队列判空判满(选择题)
循环队列中,判断队空的条件是( )。
A.front == rear
B.(rear + 1) % N == front
C.front == 0
D.rear == N - 1
答案:A
真题3:BFS 求最短路径(编程题)
给定一个 n×m 的迷宫,
#表示墙,.表示通路,求从起点 S 到终点 E 的最短步数。
#include <iostream>
#include <queue>
using namespace std;
struct Node { int x, y, step; };
int n, m;
char maze[105][105];
bool vis[105][105];
int dx[] = {-1, 1, 0, 0};
int dy[] = {0, 0, -1, 1};
int main() {
cin >> n >> m;
int sx, sy, ex, ey;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> maze[i][j];
if (maze[i][j] == 'S') { sx = i; sy = j; }
if (maze[i][j] == 'E') { ex = i; ey = j; }
}
}
queue<Node> q;
q.push({sx, sy, 0});
vis[sx][sy] = true;
while (!q.empty()) {
Node cur = q.front();
q.pop();
if (cur.x == ex && cur.y == ey) {
cout << cur.step << endl;
return 0;
}
for (int i = 0; i < 4; i++) {
int nx = cur.x + dx[i];
int ny = cur.y + dy[i];
if (nx >= 0 && nx < n && ny >= 0 && ny < m &&
!vis[nx][ny] && maze[nx][ny] != '#') {
vis[nx][ny] = true;
q.push({nx, ny, cur.step + 1});
}
}
}
cout << -1 << endl;
return 0;
}
真题4:滑动窗口最大值(代码填空)
下面代码用单调队列求滑动窗口最大值,横线处应填入( )。
while (!dq.empty() && nums[dq.back()] <= nums[i]) { ______; }答案:
dq.pop_back()
十一、易错点汇总
- 空队列调用 front/pop:必须先
empty()判断。 - STL pop 不返回值:
pop()只删除,不返回,需要先front()。 - 循环队列判空判满混淆:队空
front == rear,队满(rear + 1) % N == front。 - 循环队列取模方向错误:入队
rear = (rear + 1) % N,出队front = (front + 1) % N。 - BFS 忘记标记 visited:入队时就要标记,否则会重复入队导致死循环。
- BFS 步数更新错误:步数在入队时更新,
cur.step + 1。 - 单调队列存值还是存下标:通常存下标,方便判断是否超出窗口。
- 单调队列维护方向错误:求最大值维护单调递减,求最小值维护单调递增。
- 优先队列小根堆写法:
priority_queue<int, vector<int>, greater<int>>。 - deque 与 queue 混淆:
deque两端都可以操作,queue只能队尾入队头出。 - 数组模拟队列大小不足:数组要开足够大,防止溢出。
- 模拟排队空闲处理:队列为空时,时间跳到下一个顾客到达时间。
十二、速查表
| 需求 | 数组模拟 | STL queue | STL deque | priority_queue |
|---|---|---|---|---|
| 定义 | int q[N]; int front=0, rear=0; |
queue<int> q; |
deque<int> dq; |
priority_queue<int> pq; |
| 入队 | q[rear++] = x; |
q.push(x); |
push_back(x) / push_front(x) |
pq.push(x); |
| 出队 | front++; |
q.pop(); |
pop_front() / pop_back() |
pq.pop(); |
| 取队头 | q[front] |
q.front(); |
dq.front(); |
pq.top(); |
| 取队尾 | q[rear-1] |
q.back(); |
dq.back(); |
无 |
| 判空 | front == rear |
q.empty(); |
dq.empty(); |
pq.empty(); |
| 大小 | rear - front |
q.size(); |
dq.size(); |
pq.size(); |
| 头文件 | 无 | <queue> |
<deque> |
<queue> |
| 循环队列 | (rear+1)%N == front 判满 |
— | — | — |
| BFS | 用队列逐层扩展 | q.push() / q.pop() |
||
| 滑动窗口 | 用双端队列维护单调 | — | deque 实现 |
|
| 优先队列 | 用堆实现 | — | 默认大根堆,greater<int> 小根堆 |
十三、总结
队列是 GESP 六级数据结构模块的核心内容。
- 队列是先进先出(FIFO)的线性结构,队尾入队,队头出队。
- 数组模拟队列:
front和rear记录队头和队尾,注意假溢出。 - 循环队列用取模解决假溢出:队空
front == rear,队满(rear+1)%N == front。 - STL
queue:push入队、pop出队、front取队头、back取队尾、empty判空、size求大小。 pop()不返回被删除的值,需要先front()再pop()。deque是双端队列,两端都可以插入和删除。priority_queue是优先队列,默认大根堆,每次出队优先级最高的元素。- BFS 用队列逐层扩展,保证第一次到达终点时步数最少。
- 滑动窗口最大值用单调队列(双端队列)维护,时间复杂度 O(n)。
- 模拟排队用队列保证先到先服务,注意处理空闲时间。
复习建议:先把数组模拟队列和 STL queue 的基本操作写熟;再重点攻克循环队列的判空判满和取模操作;然后理解 deque 和 priority_queue 的用法;最后练习 BFS 和滑动窗口两个经典应用。考试中先判断题目考的是基本操作、循环队列、BFS 还是单调队列,再套用对应模板作答。