CSP-J 初赛(以满分为目标):第二十课 《循环队列——为什么队列“绕一圈”,反而能节省空间?》


第二十课:循环队列

——为什么队列“绕一圈”,反而能节省空间?

这一课是 CSP-J 初赛程序阅读、选择题、填空题经常考察的内容

本课中专门介绍了循环队列,并且给出了:

  • front

  • rear

  • % MAXN

  • 队空判断

  • 队满判断

  • 队列元素个数计算

  • 循环入队

  • 循环出队

以及相应的练习题,课后要掌握

队空:front == rear
队满:(rear + 1) % MAXN == front
元素个数:(rear - front + n) % n

所以本课我们不只是“讲循环队列”,而是重点训练大家:

看到 frontrear% 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;

注意:

这里的 frontrear位置

本课中的约定:

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的数组来实现循环队列,当前 rearfront 分别为 03,从队列删除一个元素,再插入两个元素后,rearfront 分别是多少?

选项:

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
↑                 ↓
└─────────────────┘

frontrear就像两个小机器人。

front:负责找“谁先出去”

rear:负责找“新成员放哪里”

两个机器人都一直向前走。

走到:

4

之后:

% 5

让它们重新回到:

0

二十六、和上一课的链式队列进行比较

现在我们已经学过两种队列:

链式队列

front                 rear
 ↓                      ↓
10 → 20 → 30 → 40 → NULL

循环队列

       0   1   2   3   4
       ↓   ↓   ↓   ↓   ↓
      ┌───┬───┬───┬───┬───┐
      │   │   │   │   │   │
      └───┴───┴───┴───┴───┘
       ↑               ↑
     front            rear

区别:

链式队列循环队列
存储方式链表数组
是否连续不需要数组空间
是否需要 %通常不需要需要
主要指针front/rearfront/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,然后一步一步模拟。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

汉克老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值