C++循环队列——让有限的空间循环使用
较难12循环队列:让数组空间像摩天轮一样循环使用
你有没有遇到过这种情况:在食堂排队打饭,队伍一开始排到窗口,打完饭的人从窗口离开,队伍前面空出一大段,但后面新来的人只能继续往后排,结果队伍越来越长,而前面空出来的地方却没人走上去?如果你让队伍变成圆形的,大家绕着圆圈走,就没这个问题了。
在编程里,用数组实现队列时也有类似的“空间浪费”。普通队列用数组,元素从尾部添加、从头部移除,每移除一个元素,头部就空出一个位置,但新元素只能继续往尾部加——于是数组头部空出来的位置永远用不上,而尾部却很快碰到了数组末尾。循环队列就是把这个数组想象成首尾相连的圆环,当尾部到达数组最后时,如果前面有空位,它就直接跳到开头继续存放新元素——就像摩天轮的轿厢转了一圈又回到起点。
循环队列的核心思想
循环队列也用数组存储,但用两个索引(可以理解为指针)来标记“队首”和“队尾”,再额外记录当前元素个数。关键点:
- 队首(front):指向队列中第一个元素的位置。
- 队尾(rear):指向下一个要插入元素的位置(也就是当前最后一个元素的下一个位置)。
- 元素个数(count):队列里实际有的元素数量。
初始时,front 和 rear 都指向数组下标 0,count = 0。
当入队时,把新元素放到 rear 位置,然后 rear 往后移一位。如果 rear 移到了数组末尾,就用取模运算让它回到 0。出队时也一样,把 front 往后移一位(也要循环)。
为什么用两个索引?
因为要区分队列的空和满。如果只用 front 和 rear 相等来判断空,那满的时候也会相等(整个数组都被占满时,rear 绕了一圈又追上 front)。所以我们用额外的 count 来记录元素个数,这样 front == rear 只表示空(count == 0),而 count == capacity 才表示满。
生活中的类比:操场上的“蛇形跑”
想象一个操场,一圈有 8 个站位,标号 0~7。一开始你在第 0 站(front = 0),你后面跟着第一个同学(rear = 0,还没人)。
- 每来一个新同学,就让新同学站到
rear位置,然后rear往后移一个站位。 - 如果你要离开队伍(出队),就从
front位置走人,然后front也往后移一个站位。 - 当
rear走到第 7 站,再有人来,如果front前面还有空位(比如第 0 站已经没人了),那rear就直接跳回第 0 站——循环!
这就是取模运算 (rear + 1) % capacity 的作用。它保证了索引在 0 到 capacity-1 之间来回滚动。
新手最容易犯的 3 个错误
| 错误 | 为什么错 | 正确做法 |
|---|---|---|
1. 忘记用 % 循环 | 数组下标越界,程序崩溃 | 每次移动索引都 (index + 1) % capacity |
2. 只靠 front == rear 判断满 | 空和满都满足这个条件,会混淆 | 用单独变量 count 或保留一个空位 |
3. 出队时只移动 front 不更新 count | 队列实际元素个数错误 | 出队时 count--,入队时 count++ |
完整可运行的代码示例
下面我们用一段完整的 C++ 代码实现循环队列,每个变量都加了中文注释,方便理解。代码中加入了更多生活中的例子:比如用循环队列记录最近 5 条游戏消息。
#include <iostream>
using namespace std;
class CircularQueue {
private:
int *arr; // 存储元素的数组
int capacity; // 数组最大容量
int front; // 队首索引(指向第一个元素)
int rear; // 队尾索引(指向下一个插入位置)
int count; // 当前队列中的元素个数
public:
// 构造函数:初始化容量为 size 的循环队列
CircularQueue(int size) {
capacity = size;
arr = new int[capacity];
front = 0;
rear = 0;
count = 0;
}
// 析构函数:释放动态数组
~CircularQueue() {
delete[] arr;
}
// 入队:把 value 放入队尾
// 返回 true 表示成功,false 表示队列已满
bool enqueue(int value) {
if (count == capacity) {
return false; // 队列满了,无法添加
}
arr[rear] = value; // 放到当前 rear 位置
rear = (rear + 1) % capacity; // rear 循环后移
count++; // 元素个数+1
return true;
}
// 出队:移除队首元素
// 返回 true 表示成功,false 表示队列为空
bool dequeue() {
if (count == 0) {
return false; // 队列空了,无法移除
}
front = (front + 1) % capacity; // front 循环后移
count--; // 元素个数-1
return true;
}
// 查看队首元素的值(不出队)
// 如果队列为空,返回 -1(假设元素都是非负整数)
int frontValue() {
if (count == 0) {
return -1; // 空队列,没有队首
}
return arr[front];
}
// 判断队列是否为空
bool isEmpty() {
return count == 0;
}
// 返回当前队列中的元素个数
int size() {
return count;
}
};
int main() {
// 创建一个容量为 5 的循环队列
CircularQueue messages(5);
// 模拟游戏中的聊天消息:连续收到6条消息,最后一条会失败(队列满)
cout << "尝试显示最近5条游戏消息:" << endl;
messages.enqueue(101); // 第一条消息:玩家A说"你好"
messages.enqueue(102); // 第二条消息:玩家B说"吃了吗"
messages.enqueue(103); // 第三条消息:玩家C说"组队吗"
messages.enqueue(104); // 第四条消息:玩家D说"我去打野"
messages.enqueue(105); // 第五条消息:玩家E说"我来了"
bool result = messages.enqueue(106); // 第六条消息:队列已满,入队失败!
if (!result) {
cout << "队列满了,第6条消息被丢弃" << endl;
}
// 逐个显示并移除所有消息
cout << "历史消息(按接收顺序):";
while (!messages.isEmpty()) {
cout << messages.frontValue() << " ";
messages.dequeue();
}
cout << endl;
// 输出:101 102 103 104 105 (第6条被丢弃)
// 演示循环再利用:先出队2条,再入队2条
cout << "\n演示循环再利用:" << endl;
CircularQueue cq(4); // 容量为4
cq.enqueue(10);
cq.enqueue(20);
cq.enqueue(30);
cout << "原始队列:";
cq.enqueue(40); // 此时队列为:10 20 30 40
while (!cq.isEmpty()) {
cout << cq.frontValue() << " ";
cq.dequeue();
}
cout << endl;
// 重新入队并利用前端空位
cq.enqueue(50); // 此时 front=0, rear=0, 队列空
cq.enqueue(60);
cq.enqueue(70);
cq.dequeue(); // 移除 50
cq.enqueue(80); // 可以放进去吗?能!因为 front 移开了,reear 循环到位置0
// 现在队列内容:60 70 80 (容量4,实际3个)
cout << "循环后队列:";
while (!cq.isEmpty()) {
cout << cq.frontValue() << " ";
cq.dequeue();
}
cout << endl; // 输出:60 70 80
return 0;
}
运行结果:
尝试显示最近5条游戏消息:
队列满了,第6条消息被丢弃
历史消息(按接收顺序):101 102 103 104 105
演示循环再利用:
原始队列:10 20 30 40
循环后队列:60 70 80
小思考:如何实现“丢弃旧消息”的循环队列?
有时候我们想要一个“固定大小的消息队列”,如果队列满了,新消息自动覆盖最旧的消息(就像游戏里的聊天记录只保留最后N条)。我们不需要返回 false,而是直接把队首“挤掉”。
实现思路很简单:在 enqueue 时,如果队列已满,就先 dequeue 一次,然后再正常入队。但注意:出队和入队都是 O(1) 的,所以总体还是 O(1)。
修改后的 enqueue 可以这样写:
bool enqueue(int value) {
if (count == capacity) {
// 队列已满,先丢弃队首
front = (front + 1) % capacity;
count--; // 元素个数减1(不过下面会加回来)
}
arr[rear] = value;
rear = (rear + 1) % capacity;
count++;
return true; // 永远成功
}
试试用这段代码运行上面的例子,第6条消息 106 就会成功入队,同时丢弃最早的 101,最终队列变成 102 103 104 105 106。
相关知识点指引
学会了循环队列,可以继续挑战:
- 双端队列(deque):能够在队首和队尾两头插入删除,更灵活。
- 优先级队列(priority queue):每次出队的是最大或最小元素,常用来处理“紧急任务”。
- 约瑟夫问题(Josephus Problem):用循环队列可以轻松模拟“数到M就淘汰”的游戏。
- 循环队列与数组的区别:为什么用数组而不用链表?因为数组连续内存,访问快,且循环队列可以避免动态扩容带来的性能损失。
循环队列是面试中常考的经典数据结构,也是操作系统、网络缓冲区等底层实现的基础。把它想成一个“能重复利用位置”的环形跑道,你就学会了!
例题精讲
在牺牲一个存储单元的循环队列中,队头指针为front,队尾指针为rear,队列容量为M,判断队满的条件是?
在循环队列中,当队头指针front等于队尾指针rear时,表示队列已满。
以下为循环队列的入队函数实现,队列结构体定义为:struct Queue { int *data; int front; int rear; int size; }; 其中size为队列容量,采用牺牲一个存储单元的方式判断队满。请在横线处填空。
bool enqueue(Queue &q, int x) {
if (___(1)___) return false;
q.data[q.rear] = x;
q.rear = ___(2)___;
return true;
}下列关于循环队列优点的描述,最准确的是?
以下为循环队列的出队函数实现,队列结构体同上,采用牺牲一个存储单元的方式判断队空。请在横线处填空。
int dequeue(Queue &q) {
if (___(1)___) return -1;
int x = q.data[q.front];
q.front = ___(2)___;
return x;
}