
第二十课:循环队列
——为什么队列“绕一圈”,反而能节省空间?
这一课是 CSP-J 初赛程序阅读、选择题、填空题经常考察的内容。
本课中专门介绍了循环队列,并且给出了:
-
front -
rear -
% MAXN -
队空判断
-
队满判断
-
队列元素个数计算
-
循环入队
-
循环出队
以及相应的练习题,课后要掌握
队空:
front == rear
队满:(rear + 1) % MAXN == front
元素个数:(rear - front + n) % n
所以本课我们不只是“讲循环队列”,而是重点训练大家:
看到
front、rear、% n,能够在脑子里把整个队列运行过程模拟出来。
一、先从一个问题开始
前面我们学过普通队列。
比如有一个数组:
int q[5];
我们把它当成队列:
下标:
0 1 2 3 4
↓ ↓ ↓ ↓ ↓
10 20 30 40 50
↑ ↑
front rear
现在:
10出队
20出队
变成:
0 1 2 3 4
↓ ↓ ↓ ↓ ↓
空 空 30 40 50
↑ ↑
front rear
接下来加入:
60
70
问题来了:
前面的
0、1明明已经空出来了,为什么不能继续使用?
这就是普通数组模拟队列的一个问题:
空间浪费!
线性数组模拟队列在元素不断出队以后,前面的空间会出现很多空位,导致空间浪费,因此需要把数组“首尾相接”模拟成环。
二、解决办法:让数组变成一个圆
想象一下:
普通数组:
0 → 1 → 2 → 3 → 4
我们把它弯起来:
0
↗ ↘
4 1
↑ ↓
3 ← 2
于是:
4的下一个不是5
而是0
这就是:
循环队列
三、循环队列的核心思想
循环队列最重要的一句话:
走到数组最后一个位置以后,从头开始继续走。
例如数组大小:
const int N = 5;
下标:
0 1 2 3 4
那么:
4的下一个位置
是:
0
怎么计算?
(4 + 1) % 5
结果:
0
所以:
% 是循环队列的“魔法”。
四、为什么 (rear + 1) % MAXN?
这是本课最重要的代码之一。
rear = (rear + 1) % MAXN;
假设:
MAXN = 5;
那么:
rear = 0
→ 1
→ 2
→ 3
→ 4
→ 0
→ 1
→ ...
是不是就像一个人在操场跑步?
0 → 1 → 2 → 3 → 4
↑ ↓
└───────────────┘
所以:
% MAXN就是让指针“绕圈”。
五、先认识两个指针
循环队列最重要的两个变量:
int front = 0;
int rear = 0;
注意:
这里的 front 和 rear 是位置。
本课中的约定:
front
↓
队头元素的位置
rear
↓
下一个元素应该放入的位置
因此:
front
↓
10 20 30
↑
rear
这里 rear 并不是30的位置,而是:
30后面那个“准备放新元素”的位置。
这一点我们按照这种方式来定义。
六、为什么rear不是最后一个元素?
假设:
front = 0
rear = 3
数组:
下标: 0 1 2 3 4
┌───┬───┬───┬───┬───┐
│10 │20 │30 │ │ │
└───┴───┴───┴───┴───┘
↑ ↑
front rear
队列里面是:
10 20 30
而:
rear = 3
意味着:
下一次入队,要放到
q[3]。
七、入队操作
核心代码是:
queue[rear] = value;
rear = (rear + 1) % MAXN;
对应循环队列实现。
我们一步一步看。
第一步:放进去
假设:
front = 0
rear = 3
执行:
addqueue(40);
先:
queue[rear] = 40;
也就是:
queue[3] = 40;
变成:
10 20 30 40 空
↑ ↑
front rear
但是此时 rear 还是3。
第二步:rear向后移动
执行:
rear = (rear + 1) % MAXN;
得到:
rear = (3 + 1) % 5
= 4
所以:
10 20 30 40 空
↑ ↑
front rear
八、当rear走到最后怎么办?
假设:
front = 3
rear = 4
现在:
下标:
0 1 2 3 4
↑ ↑
front rear
执行:
queue[rear] = 50;
就是:
queue[4] = 50;
然后:
rear = (rear + 1) % 5;
得到:
rear = 0
于是:
0 1 2 3 4
↑ ↑
rear front
是不是“绕回来了”?
这就是循环队列。
九、出队操作
循环队列出队非常简单。
核心代码:
r = queue[front];
front = (front + 1) % MAXN;
并把原位置标记为空。
例如:
front
↓
10 20 30 40
执行:
delqueue();
先取:
r = queue[front];
所以:
r = 10
然后:
front = (front + 1) % MAXN;
如果:
front = 0
那么:
front = 1
于是:
10 20 30 40
↑
front
10就出队了。
十、出队也会“绕圈”
假设:
front = 4
执行:
front = (front + 1) % 5;
得到:
front = 0
所以:
4 → 0
这就是循环。
十一、循环队列最重要的两个状态
现在进入CSP-J最重要的内容。
循环队列需要判断:
1. 队空了吗?
2. 队满了吗?
十二、队空条件
当出现:
front == rear
表示:
队空
队空:rear == front
例如:
front
↓
rear
↓
0 1 2 3 4
说明:
没有任何元素。
十三、那么队满呢?
这里就有一个非常有意思的问题。
如果:
front == rear
是队空。
那么:
队满的时候,会不会也出现
front == rear?
答案:
会!
这就是麻烦所在。
十四、为什么队满也可能front==rear?
假设数组大小为5。
如果我们允许把5个位置全部放满:
10 20 30 40 50
↑ ↑
front rear
绕一圈以后:
front == rear
那么计算机就分不清:
这是空队列?
还是:
这是满队列?
所以需要一个特殊规则。
十五、解决方法:故意浪费一个位置
这是循环队列非常经典的思想。
假设数组:
0 1 2 3 4
虽然有5个位置:
我们只允许真正存4个元素。
故意空出一个位置。
这样就可以区分:
队空
和:
队满
为解决“队空”和“队满”状态无法区分的问题,入队时少使用一个数据元素空间。
十六、队满条件
在这种约定下:
(rear + 1) % MAXN == front
表示:
队满
这是讲义中特别强调的公式。
一定要让孩子记住:
队空:
rear == front
队满:
(rear + 1) % MAXN == front
十七、用一个小例子理解队满
假设:
MAXN = 5;
现在:
front = 0
rear = 4
那么:
(rear + 1) % MAXN
就是:
(4 + 1) % 5
= 0
恰好:
0 == front
所以:
队满!
但是实际上只有:
4个元素
而不是5个。
这就是:
循环队列牺牲一个数组位置,换来了简单可靠的“空/满判断”。
十八、一个非常重要的公式:队列元素个数
如果:
front = f
rear = r
数组长度:
n
那么元素个数:
(r - f + n) % n
也就是:
(rear - front + n) % n
公式是:
(r-f+n) mod n
十九、为什么需要+n?
这是初学者最容易问的问题。
例如:
front = 3
rear = 1
显然队列可能是:
3 → 4 → 0
所以有:
3个元素?
计算:
rear - front
= 1 - 3
= -2
显然不对。
所以:
rear - front + n
如果:
n = 5
得到:
1 - 3 + 5
= 3
再:
3 % 5
= 3
答案就是:
3
二十、为什么最后还要 % n?
因为:
我们是在一个“圆”里面计算。
例如:
rear = 8
front = 3
n = 5
直接:
8 - 3 = 5
但是在循环队列里面位置实际上不断绕圈。
所以使用:
(rear - front + n) % n
把结果重新限制在:
0 ~ n-1
二十一、程序阅读神器:看到这个式子马上想到什么?
如果我们在初赛看到:
(rear - front + n) % n
应该立刻想到:
循环队列中的元素数量。
看到:
(rear + 1) % n == front
应该想到:
队满。
看到:
rear == front
应该想到:
队空。
看到:
rear = (rear + 1) % n;
应该想到:
入队后,队尾指针向后移动一格。
看到:
front = (front + 1) % n;
应该想到:
出队后,队头指针向后移动一格。
演示代码:
#include <iostream>
using namespace std;
#define MAXN 5
// ========================
// 循环队列
// ========================
// 队列数组
int queue[MAXN] = {0};
// 队头指针
int front = 0;
// 队尾指针
int rear = 0;
// ========================
// 入队
// ========================
void addqueue(int value)
{
// 如果下一个位置就是 front,
// 说明队列已经满了
if ((rear + 1) % MAXN == front)
{
cout << "队满" << endl;
return;
}
// 把元素放入队尾
queue[rear] = value;
// rear 向后移动
// 到数组末尾后回到 0
rear = (rear + 1) % MAXN;
}
// ========================
// 出队
// ========================
int delqueue()
{
// front == rear
// 表示队列为空
if (front == rear)
{
cout << "队空" << endl;
return -1;
}
// 保存队头元素
int value = queue[front];
// 出队后进行标记
queue[front] = -1;
// front 向后移动
front = (front + 1) % MAXN;
return value;
}
// ========================
// 显示队列
// ========================
void display()
{
// 队空
if (front == rear)
{
cout << "队空" << endl;
return;
}
// 从队头开始显示
int i = front;
do
{
cout << queue[i] << " ";
// 向后移动
// 到数组末尾后回到 0
i = (i + 1) % MAXN;
} while (i != rear);
cout << endl;
}
// ========================
// 主函数
// ========================
int main()
{
int order; // 用户输入的指令
int value; // 入队的元素
int t; // 保存出队结果
cout << "输入指令:" << endl;
while (true)
{
cout << "1 入队,2 出队,3 显示!" << endl;
cin >> order;
// ====================
// 1. 入队
// ====================
if (order == 1)
{
cin >> value;
addqueue(value);
display();
}
// ====================
// 2. 出队
// ====================
else if (order == 2)
{
t = delqueue();
if (t != -1)
{
cout << t << " 已经出队" << endl;
display();
}
else
{
cout << "队列已空" << endl;
}
}
// ====================
// 3. 显示
// ====================
else if (order == 3)
{
display();
}
// ====================
// 其他指令
// ====================
else
{
cout << "口令错误!" << endl;
}
}
return 0;
}
二十二、来做典型题
有这样一道题:
若用一个大小为6的数组来实现循环队列,当前
rear和front分别为0和3,从队列删除一个元素,再插入两个元素后,rear和front分别是多少?
选项:
A. 4 和 2
B. 2 和 4
C. 3 和 0
D. 0 和 3
我们不要乱猜。
第一步:初始状态
rear = 0
front = 3
第二步:删除一个元素
删除后:
front = (front + 1) % 6;
所以:
front = (3 + 1) % 6
= 4
此时:
rear = 0
front = 4
第三步:插入第一个元素
rear = (rear + 1) % 6;
所以:
rear = 1
第四步:插入第二个元素
rear = (1 + 1) % 6
= 2
所以最后:
rear = 2
front = 4
答案:
B
二十三、要注意:
有的同学会认为一直是:
rear++
就是:
0 → 1 → 2 → 3 → 4 → 5
但循环队列不是。
必须要时刻想到:
(rear + 1) % n
所以是:
4 → 0
而不是:
4 → 5
二十四、循环队列核心代码
const int MAXN = 5;
int q[MAXN];
int front = 0;
int rear = 0;
入队:
void push(int x)
{
if((rear + 1) % MAXN == front)
{
cout << "队满" << endl;
return;
}
q[rear] = x;
rear = (rear + 1) % MAXN;
}
出队:
int pop()
{
if(front == rear)
{
cout << "队空" << endl;
return -1;
}
int x = q[front];
front = (front + 1) % MAXN;
return x;
}
这里最值得孩子观察的不是代码长度,而是:
入队:
q[rear]
rear向后
出队:
q[front]
front向后
二十五、把循环队列想成“跑步机”
我们在跑步机上跑跑看。
普通数组:
0 → 1 → 2 → 3 → 4 → 撞墙
循环队列:
0 → 1 → 2 → 3 → 4
↑ ↓
└─────────────────┘
front和rear就像两个小机器人。
front:负责找“谁先出去”
rear:负责找“新成员放哪里”
两个机器人都一直向前走。
走到:
4
之后:
% 5
让它们重新回到:
0
二十六、和上一课的链式队列进行比较
现在我们已经学过两种队列:
链式队列
front rear
↓ ↓
10 → 20 → 30 → 40 → NULL
循环队列
0 1 2 3 4
↓ ↓ ↓ ↓ ↓
┌───┬───┬───┬───┬───┐
│ │ │ │ │ │
└───┴───┴───┴───┴───┘
↑ ↑
front rear
区别:
| 链式队列 | 循环队列 | |
|---|---|---|
| 存储方式 | 链表 | 数组 |
| 是否连续 | 不需要 | 数组空间 |
是否需要 % | 通常不需要 | 需要 |
| 主要指针 | front/rear | front/rear |
| 是否固定容量 | 通常不是 | 是 |
| 核心难点 | 指针 | 下标循环 |
所以:
“队列”是规则,“循环队列”是实现方法。
二十七、CSP-J容易出现的陷阱①
看到:
rear = (rear + 1) % n;
不要简单理解成:
rear++;
要真正理解:
rear向后移动一格,如果越过最后位置,就回到0。
二十八、陷阱②:rear到底指什么?
本课我们的定义:
rear
指向:
下一个入队元素应该存放的位置。
所以:
q[rear] = x;
rear = (rear + 1) % n;
是一个完整动作。
千万不要看到:
rear = 3
就认为:
“第3号位置就是最后一个元素。”
在这种实现里,恰恰可能是:
rear指向的是下一个空位置。
二十九、陷阱③:队满为什么不是 front == rear?
因为:
front == rear
已经被定义成:
队空。
为了区分空和满:
浪费一个位置。
所以:
(rear + 1) % n == front
才表示:
队满。
三十、陷阱④:循环队列容量不是n
如果:
int q[5];
并采用:
(rear + 1) % 5 == front
判断队满。
那么最多只能存:
4个元素
而不是:
5个。
这个问题在初赛中很容易出现。
三十一、队列基本概念
经典题:
“( )是一种先进先出的线性表。”
选项:
A 栈
B 队列
C 哈希表
D 二叉树
答案:
B
因为:
队列 = FIFO
而:
栈 = LIFO
三十二、再来一道题:第五个出队是谁?
已知队列为
(13,2,11,34,41,77,5,7,18,26,15),第一个进入队列的是13,问第五个出队元素。
队列:
13
↓
2
↓
11
↓
34
↓
41
↓
77
↓
...
先进先出。
所以:
第1个:13
第2个:2
第3个:11
第4个:34
第5个:41
答案:
41
这个题看起来很简单,但实际上是在训练:
不要被数组、指针、代码干扰,先抓住数据结构本身的规则。
三十三、综合模拟题
假设:
const int N = 6;
int front = 0;
int rear = 0;
依次:
入队10
入队20
入队30
出队
入队40
入队50
我们一步一步走。
初始
front = 0
rear = 0
入队10
放到:
q[0]
然后:
rear = 1
入队20
q[1] = 20
rear = 2
入队30
q[2] = 30
rear = 3
现在:
10 20 30
↑ ↑
front rear
出队
10出去。
front = (0 + 1) % 6
= 1
现在:
10 20 30
↑ ↑
front rear
入队40
放:
q[3] = 40
然后:
rear = 4
入队50
q[4] = 50
rear = 5
最终:
10 20 30 40 50
↑ ↑
front rear
实际队列:
20 30 40 50
一共:
4个元素
公式验证:
(rear - front + N) % N
就是:
(5 - 1 + 6) % 6
= 10 % 6
= 4
完全正确。
三十四、再来一个“绕圈”题
假设:
N = 6
front = 4
rear = 2
那么队列元素个数:
(rear - front + N) % N
代入:
(2 - 4 + 6) % 6
= 4
所以:
队列有4个元素。
它们实际上位于:
4 → 5 → 0 → 1
也就是:
下标:
0 1 2 3 4 5
↑ ↑ ↑ ↑
3? 4? front
更准确地看:
front
↓
[4] → [5] → [0] → [1]
↑
rear
注意:
rear=2表示下一个准备存放的位置是2。
所以元素是:
4、5、0、1
三十五、我们为什么不能直接做 rear-front
如果:
front = 4
rear = 2
直接:
rear - front
得到:
-2
明显不可能是元素数量。
所以:
(rear - front + n) % n
其实是在解决:
“队尾已经绕到队头前面去了怎么办?”
三十六、本课和前面知识的连接
现在同学们应该能够把:
数组
↓
下标
↓
%
↓
指针思想
↓
队列
↓
循环队列
全部串起来。
尤其是:
(rear + 1) % n
本质并不是一个奇怪的公式。
它只是:
“下一个位置,如果到头了,就回到开头。”
三十七、本课CSP-J必背“四件套”
如果时间有限,这几个大家要记住:
① 队空
front == rear
② 队满
(rear + 1) % n == front
③ 入队
q[rear] = x;
rear = (rear + 1) % n;
④ 出队
x = q[front];
front = (front + 1) % n;
再加一个:
⑤ 元素个数
(rear - front + n) % n
三十八、给大家一个“口诀”
队空看相等,队满看下一格;
rear负责进,front负责出;
走到最后别撞墙,% n绕回第一行;
队伍数量别硬减,加上n再取模。
或者更短:
空相等,满看下一格;
rear进,front出;
加n取模,绕圈不迷路。
三十九、课堂练习
练习1:判断队列类型
“先进先出”的数据结构是什么?
练习2:计算指针
int n = 5;
int rear = 4;
rear = (rear + 1) % n;
问:
rear是多少?
答案:
0
练习3:计算元素个数
n = 8
front = 6
rear = 3
求:
队列元素个数?
计算:
(3 - 6 + 8) % 8
= 5
答案:
5
练习4:判断队满
n = 7
front = 3
rear = 2
判断:
(rear + 1) % n == front
即:
(2 + 1) % 7 == 3
成立。
所以:
队满。
练习5:综合模拟
int n = 5;
int front = 0;
int rear = 0;
依次:
push(10)
push(20)
push(30)
pop()
push(40)
push(50)
同学们画出每一步:
front
rear
数组内容
最后问:
front是多少?
rear是多少?
队列里有几个元素?
队头是什么?
队尾是什么?
这道题非常适合做“程序阅读模拟”。
四十、一定要清楚一个思想
学到这里,很容易把循环队列记成一堆公式:
(rear + 1) % n
(rear - front + n) % n
(rear + 1) % n == front
但真正应该要理解的是:
循环队列
│
┌────────┴────────┐
↓ ↓
front rear
│ │
负责出 负责进
│ │
└──────┬──────────┘
↓
数组首尾相连
↓
% n
↓
绕圈前进
所以这些公式并不是“背出来的”。
它们都是从一个非常简单的想法推出来的:
数组最后一个位置的下一个位置,是第0个位置。
四十一、本课知识树
队列
│
先进先出 FIFO
│
┌────────┴────────┐
↓ ↓
链式队列 顺序队列
│
↓
循环队列
│
┌─────────┼─────────┐
↓ ↓ ↓
front rear %
│ │ │
↓ ↓ ↓
出队 入队 绕圈
队空:
front == rear
队满:
(rear + 1) % n == front
元素个数:
(rear - front + n) % n
📌 本课与CSP-J初赛的对应重点。
本课的学习目标,不要停留在“会写循环队列”,而应该达到:
看到一段循环队列代码,同学们能够画出数组、标出front和rear,然后一步一步模拟。
1万+

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



