Files

120 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## C++ 算法讲义:队列
### 1. 引言
* **问题引入**:
例 15-7:超市排队。这回小止作为一个收银员在超市打工。收银员会给排在队伍最前面的顾客结账,然后再服务队伍中的下一个顾客。同时,队伍的末尾一直会有新的顾客加入。
**设问**:如果我们在超市排队,收银员像处理栈一样,总是先给**最后面**来的人结账(后进先出),会发生什么?
前面排队的人绝对会掀桌子!这太不公平了!
* **价值阐述**:
为了维持秩序,我们需要一种公平的规则——**谁先排队,谁先结账**。这就是**队列(Queue)**的核心思想。在计算机世界里,操作系统的任务调度、网络数据的传输等,都需要这种公平的机制来维持运转。
* **学习目标**:
掌握队列先进先出的运作机制,学会用数组模拟队列,并熟练使用 C++ STL 中的 `queue` 模板解决环形逻辑问题。
---
### 2. 核心思想
队列(queue)是一种遵循**先进先出(First In, First Out,简称 FIFO)**规则的线性数据结构。顾名思义,它模拟了现实中的排队现象。
我们将队列的核心操作与超市排队完美对应:
* **入队 `push(x)` —— 排队入列**
* **比喻**:新顾客 $x$ 推着购物车,站到了队伍的最末尾。
* **意义**:将元素 $x$ 压入队列的尾部。
* **看队首 `front()` —— 抬头看一眼**
* **比喻**:收银员小止喊:“下一位!” 然后抬头看一眼现在站在最前面的人是谁。注意,**只是看一眼**,这个人还没结账走掉。
* **意义**:查询(获取)队首的元素,但不删除它。
* **出队 `pop()` —— 结账走人**
* **比喻**:最前面的人结完账,离开了队伍。排在第二位的人顺理成章地成为了新的队首。
* **意义**:弹出队首的元素(删除最前面的数据)。
---
### 3. 代码实现
#### 方法一:使用数组模拟队列
在程序里,我们可以用一个固定大小的数组和两个变量来管理队伍。
* **初始化**:
定义数组 `int q[100];`。
初始化两个指针索引:`front = 0` 和 `rear = 0`。
* `front`:指向队列的第一个人(队首)。
* `rear`:指向队伍末尾**下一个可插入的空位**。
* **入队 (`push`)**:
将新元素放入 `rear` 指向的位置:`q[rear] = x;`。
队伍变长了,尾巴往后挪一格:`rear++;`。
* **出队 (`pop`)**:
队首的人走了,前面空出来了,头指针往后挪一格:`front++;`。(我们不需要真的抹除数组里的数据,只要 `front` 越过它,它就相当于出队了)。
#### 方法二:使用 C++ 现成的模板 —— `queue`
实际编程中,我们不需要每次都手写数组,C++ 标准库已经为我们准备好了。
```cpp
#include <iostream>
#include <queue> // 队列头文件
using namespace std;
int main() {
// 声明语法:queue<数据类型> 队列名;
queue<int> q; // 存顾客编号
// queue<string> qs; 存顾客姓名
q.push(1); // 1号顾客排队
q.push(2); // 2号顾客排队
cout << "当前队首是: " << q.front() << endl; // 看一眼,输出 1
q.pop(); // 1号顾客结账走人
cout << "新的队首是: " << q.front() << endl; // 看一眼,输出 2
return 0;
}
```
---
### 4. 实例
**例 15-9 约瑟夫问题 (B0221)**
**题目**:$n$ 个人围成一圈,从第一个人开始报数,数到 $k$ 的人出列。再由下一个人重新从 1 开始报数,数到 $k$ 的人再出圈... 依次类推,直到淘汰 $n-1$ 名小朋友。请按顺序输出每个出圈人的编号。
* **思考**:
题目说小朋友坐成一个圈,我们的直觉是画一个圆去模拟。但在计算机内存里,数据通常是**排成一排**的(线性)。怎么让这一排数据能转起来呢?
* **算法**:
想象一个超市收银台的回转传送带。传送带本身是直的,但在尽头会把没被拿走的数据**送回起始点**!
对于这个传送带而言,先放进去的数据,会先传到尽头,这不就是队列吗!我们只需要加上一个特殊操作:**队首回填队尾**。
* **步骤拆解(数到 $k$ 淘汰)**:
假设队列里是 1 到 $n$。我们要数 $k$ 个人:
1. **对于前 $k-1$ 个人(不用出局)**:
操作:先获取队首元素 `x = q.front()`,把他踢出队首 `q.pop()`,紧接着让他重新在队尾排队 `q.push(x)`。
2. **对于第 $k$ 个人(必须出局)**:
操作:看一眼队首并输出他的编号,然后直接踢出去 `q.pop()`,**不再**让他回队尾。
队列 + 队首回填队尾 叫做**逻辑环形队列**,在很多需要周期性、环形逻辑的地方都有效!
---
### 5. 总结
* **要点回顾**:
* **队列 (Queue)** 是一种遵循**先进先出 (FIFO)** 原则的数据结构。
* **入队 (`push`)**:新元素从队尾进入。
* **出队 (`pop`)**:老元素从队首离开。
* **看队首 (`front`)**:获取准备第一个离开的元素。
* **看队尾 (`back`)**:获取最后进入的元素。
* **适用场景**:
只要问题符合**先产生、先处理**,需要公平排队或**逐层扩散**的逻辑,就适合用队列。例如:数据流动模型(滑动窗口)、资源调度模型(打印机排队)等。