CC++ & Algorithm

C++循环队列——让有限的空间循环使用

较难12
语言版本:C++Python
概述:循环队列是一种用数组实现的队列,当尾部到达数组末尾时,会从开头继续,像圆形跑道一样循环。

循环队列:让数组空间像摩天轮一样循环使用

你有没有遇到过这种情况:在食堂排队打饭,队伍一开始排到窗口,打完饭的人从窗口离开,队伍前面空出一大段,但后面新来的人只能继续往后排,结果队伍越来越长,而前面空出来的地方却没人走上去?如果你让队伍变成圆形的,大家绕着圆圈走,就没这个问题了。

在编程里,用数组实现队列时也有类似的“空间浪费”。普通队列用数组,元素从尾部添加、从头部移除,每移除一个元素,头部就空出一个位置,但新元素只能继续往尾部加——于是数组头部空出来的位置永远用不上,而尾部却很快碰到了数组末尾。循环队列就是把这个数组想象成首尾相连的圆环,当尾部到达数组最后时,如果前面有空位,它就直接跳到开头继续存放新元素——就像摩天轮的轿厢转了一圈又回到起点。

循环队列的核心思想

循环队列也用数组存储,但用两个索引(可以理解为指针)来标记“队首”和“队尾”,再额外记录当前元素个数。关键点:

  • 队首(front):指向队列中第一个元素的位置。
  • 队尾(rear):指向下一个要插入元素的位置(也就是当前最后一个元素的下一个位置)。
  • 元素个数(count):队列里实际有的元素数量。

初始时,frontrear 都指向数组下标 0,count = 0
当入队时,把新元素放到 rear 位置,然后 rear 往后移一位。如果 rear 移到了数组末尾,就用取模运算让它回到 0。出队时也一样,把 front 往后移一位(也要循环)。

为什么用两个索引?
因为要区分队列的空和满。如果只用 frontrear 相等来判断空,那满的时候也会相等(整个数组都被占满时,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就淘汰”的游戏。
  • 循环队列与数组的区别:为什么用数组而不用链表?因为数组连续内存,访问快,且循环队列可以避免动态扩容带来的性能损失。

循环队列是面试中常考的经典数据结构,也是操作系统、网络缓冲区等底层实现的基础。把它想成一个“能重复利用位置”的环形跑道,你就学会了!

例题精讲

1单选题

在牺牲一个存储单元的循环队列中,队头指针为front,队尾指针为rear,队列容量为M,判断队满的条件是?

A(rear+1)%M == front
Brear == front
C(rear+1)%M == 0
Dfront == (rear+1)%M
2判断题

在循环队列中,当队头指针front等于队尾指针rear时,表示队列已满。

3填空题
以下为循环队列的入队函数实现,队列结构体定义为: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;
}
4单选题

下列关于循环队列优点的描述,最准确的是?

A可以任意多次入队出队而不需要移动元素
B可以解决顺序队列的假溢出问题
C插入和删除操作的时间复杂度都是O(1)
D以上都是
5填空题
以下为循环队列的出队函数实现,队列结构体同上,采用牺牲一个存储单元的方式判断队空。请在横线处填空。
int dequeue(Queue &q) {
    if (___(1)___) return -1;
    int x = q.data[q.front];
    q.front = ___(2)___;
    return x;
}