---
marp: true
theme: kurzus
paginate: true
footer: 数据结构 · 第 8 周 · 队列
---

<!-- _class: cim -->
<!-- _paginate: false -->
<!-- _footer: "" -->

# 数据结构

## 第 8 周 – 队列：先进先出原则、循环队列、双端队列、优先队列

Koczka Ferenc 博士 · 埃斯特哈齐·卡罗伊大学 · 2026

---

## 复习

- <strong>栈：</strong>后进先出 – 只能访问栈顶
- Push、Pop、Top – 都是 <strong>O(1)</strong>，用数组时在数组末尾操作
- 应用：后缀表达式求值、括号匹配、撤销、调用栈

### 今天的问题

- 栈和队列有什么区别？
- 为什么“朴素”的数组队列不好？<strong>循环</strong>队列怎样解决这个问题？
- 什么是双端队列和优先队列？

<!--
时间安排：概念 10' · 朴素数组队列 15' · 循环队列 30' · 双端队列、优先队列 20' · 应用、小结 15'
-->

---

## 队列的概念

<strong>队列</strong>是一种在<strong>队尾</strong>插入、在<strong>队头</strong>删除的数据结构。

<strong>FIFO</strong> – <em>First In, First Out</em>（先进先出）：先进去的先出来。

<div class="ket-hasab">
<div>

### 生活中的例子

- 在食堂、收银台排队
- 打印队列
- 客服电话排队
- 键盘缓冲区

</div>
<div>

### 性质

| | |
|---|---|
| 元素 | 同构 |
| 关系 | 线性 |
| 访问 | <strong>受限</strong> – 队头和队尾 |
| 大小 | 静态或动态 |

</div>
</div>

---

## 队列的操作

![w:760](kepek/h08_muveletek.svg)

| 操作 | 含义 | 步数 |
|---|---|---|
| <strong>Enqueue</strong> | 元素加入<strong>队尾</strong> | O(1) |
| <strong>Dequeue</strong> | 删除<strong>队头</strong>元素 | O(1) |
| <strong>Peek / Front</strong> | 读取队头元素 | O(1) |
| <strong>IsEmpty?</strong> · <strong>IsFull?</strong> | 状态 | O(1) |

---

## 栈与队列的比较

| | 栈 | 队列 |
|---|---|---|
| 原则 | <strong>后进先出</strong> | <strong>先进先出</strong> |
| 插入 | 压入栈顶（push） | 加入队尾（enqueue） |
| 删除 | 从栈顶弹出（pop） | 从队头删除（dequeue） |
| 访问 | 一端 | 两端 |
| 数组实现中的指针 | `top` | `front`、`rear` |
| 典型应用 | 撤销、递归、表达式 | 调度、缓冲、广度优先遍历 |

---

<!-- _class: fejezet -->

# 用数组实现队列

---

## 第一次尝试：朴素的数组队列

`front` 是第一个元素的下标，`rear` 是最后一个元素的下标；入队：`rear ← rear + 1`，出队：`front ← front + 1`

![w:1000](kepek/h08_naiv.svg)

---

## 改进朴素队列的方法

<strong>1. 移动元素：</strong>每次出队后，把所有元素向前移动一位
- 队头始终在 0，不浪费空间
- 但出队变成了 <strong>O(n)</strong> ✗

<strong>2. 循环队列：</strong>把数组看成一个<strong>环</strong> – `rear` 到达数组末尾时，从<strong>开头</strong>继续
- 没有元素移动，每个操作都是 <strong>O(1)</strong> ✓
- 下标“绕回”:<strong>(i + 1) mod n</strong>

---

## 循环队列

![w:1000](kepek/h08_ciklikus.svg)

---

## 空还是满？循环队列的陷阱

只用 `front` 和 `rear` 时，<strong>空</strong>队列和<strong>满</strong>队列看起来一样：`front = (rear + 1) mod n`！

<strong>解决办法：</strong>

1. 记录<strong>元素个数</strong>（`count`） – 空：`count = 0`，满：`count = n` ← <em>我们采用这种</em>
2. <strong>始终留一个空位</strong> – 满：`(rear + 1) mod n = front`，但只能放 <em>n</em> − 1 个元素
3. 用一个布尔标志（`empty`），由各操作负责设置

<div class="doboz tipp">

基于计数器的方法最简单，也最不容易出错。

</div>

---

<!-- _class: kicsi -->

## 循环队列 – 伪代码

<div class="ket-hasab">
<div>

```text
// 数组 T[0..n−1](这里例外地从 0 开始,
// 这样 mod n 比较简单)

Procedure Create
  front ← 0 ; rear ← n − 1 ; count ← 0
End procedure

Function IsEmpty
  Return count = 0
End function

Function IsFull
  Return count = n
End function
```

</div>
<div>

```text
Procedure Enqueue(x)
  If IsFull then ERROR("the queue is full")
  rear ← (rear + 1) mod n
  T[rear] ← x
  count ← count + 1
End procedure

Function Dequeue
  If IsEmpty then ERROR("the queue is empty")
  x ← T[front]
  front ← (front + 1) mod n
  count ← count − 1
  Return x
End function
```

</div>
</div>

---

<!-- _class: kicsi tomor -->

## 循环队列 – Java 和 Go

<div class="ket-hasab">
<div>

<span class="cimke java">Java</span>

```java
class CircularQueue {
    private final int[] data;
    private int front = 0, rear = -1, count = 0;

    void enqueue(int x) {
        if (count == data.length)
            throw new IllegalStateException("full");
        rear = (rear + 1) % data.length;
        data[rear] = x;
        count++;
    }

    int dequeue() {
        if (count == 0)
            throw new IllegalStateException("empty");
        int x = data[front];
        front = (front + 1) % data.length;
        count--;
        return x;
    }
}
```

</div>
<div>

<span class="cimke go">Go</span>

```go
type CircularQueue struct {
    data              []int
    front, rear, count int
}

func (q *CircularQueue) Enqueue(x int) error {
    if q.count == len(q.data) {
        return errors.New("full")
    }
    q.rear = (q.rear + 1) % len(q.data)
    q.data[q.rear] = x
    q.count++
    return nil
}

func (q *CircularQueue) Dequeue() (int, error) {
    if q.count == 0 {
        return 0, errors.New("empty")
    }
    x := q.data[q.front]
    q.front = (q.front + 1) % len(q.data)
    q.count--
    return x, nil
}
```

</div>
</div>

---

## 内置的队列

<div class="ket-hasab">
<div>

<span class="cimke java">Java</span>

```java
Queue<String> q = new ArrayDeque<>();
q.offer("Li");      // 入队
q.offer("Wang");
q.peek();           // "Li"
q.poll();           // "Li" – 出队
q.isEmpty();
```

`ArrayDeque` 内部是一个<strong>循环的、可动态增长的</strong>数组。

</div>
<div>

<span class="cimke go">Go</span>

```go
q := []string{}
q = append(q, "Li")   // 入队
q = append(q, "Wang")
first := q[0]         // 查看队头
q = q[1:]             // 出队
```

简单，但 `q[1:]` 不会释放开头的空间 – 操作很多时，最好自己写循环队列或使用 `container/list`。

</div>
</div>

---

<!-- _class: fejezet -->

# 队列的变体

---

## 双端队列（deque）

![w:1000](kepek/h08_deque.svg)

- <strong>Deque</strong>（<em>double-ended queue</em>，读作“deck”）：两端都可以<strong>插入</strong>和<strong>删除</strong>
- 只用一端就是<strong>栈</strong>，用相对的两端就是<strong>队列</strong>
- 用循环数组实现：`addFirst` 时 `front ← (front − 1 + n) mod n`

<div class="doboz figyelem">

在 Java 和 Go 中 `-1 % n` 的值是 <strong>−1</strong>，而不是 <em>n</em> − 1!所以需要 `+ n`。

</div>

---

## 优先队列

每个元素都有一个<strong>优先级</strong>；我们总是取出<strong>最重要</strong>的元素（例如优先级数字最小的） – 而不是最早进入的。

例子：医院急诊、操作系统中的进程调度、路径规划（Dijkstra 算法）

| 实现 | 插入 | 取出最小值 |
|---|---|---|
| 无序数组 | <strong>O(1)</strong> – 放在末尾 | O(n) – 求最小值 |
| 有序数组 | O(n) – 移动到正确位置 | <strong>O(1)</strong> – 从末尾取 |
| <strong>堆</strong>（第 15 周） | <strong>O(log n)</strong> | <strong>O(log n)</strong> |

内置：Java `PriorityQueue` · Go `container/heap`

---

## 应用

- <strong>缓冲：</strong>键盘、网络数据包、视频流 – 平衡生产者和消费者的不同速度
- <strong>调度：</strong>打印队列、操作系统的<strong>时间片轮转</strong>调度
- <strong>模拟：</strong>收银台、交通、服务系统
- <strong>广度优先遍历</strong>（BFS）：树的层序遍历、图中的最短路径
- 分布式系统中的<strong>消息队列</strong>（例如处理网店的订单）

<div class="doboz tipp">

<strong>时间片轮转：</strong>每个进程最多运行一个时间片，然后回到队尾 – 这样每个进程都能轮到。

</div>

---

## 小结

- <strong>队列：</strong>先进先出 – 在队尾插入，在队头删除
- 操作：<strong>Enqueue、Dequeue、Peek、IsEmpty?、IsFull?</strong> – 都是 <strong>O(1)</strong>
- <strong>朴素数组队列：</strong>队列“漂移”，前面的空间被浪费
- <strong>循环队列：</strong>`(i + 1) mod n` – 绕回，不移动元素；空/满：用<strong>计数器</strong>
- <strong>双端队列：</strong>两端都可以插入和删除
- <strong>优先队列：</strong>最重要的元素先出来 – 用堆实现 O(log n)

<strong>下周：</strong>动态数据结构 – 动态内存管理模型和单链表。

---

## 复习题

1. 输出什么？`enqueue(1); enqueue(2); dequeue(); enqueue(3); enqueue(4); dequeue(); dequeue(); dequeue();`
2. 循环队列，n = 5，初始为空（front = 0,rear = 4）。执行：4 次入队，3 次出队，3 次入队。现在 `front`、`rear` 和 `count` 各是多少？
3. 对 n = 10，下面的代码输出什么？一般地，它做了什么？
   `q.enqueue(0); q.enqueue(1); for (i = 0; i < n; i++) { a = q.dequeue(); print(a); b = q.peek(); q.enqueue(a + b); }`
4. 判断对错：在循环队列中出队是 O(n) 的，因为要把元素向前移动。
5. 下列情况你会用哪种数据结构：a) 浏览器的“后退”按钮，b) 打印任务，c) 医院急诊？

<!--
答案：1. 1 2 3 4
2. 4 次入队：rear = 3,count = 4。3 次出队：front = 3,count = 1。3 次入队：rear = 4, 0, 1 → rear = 1,count = 4。所以 front = 3,rear = 1,count = 4。
3. 0 1 1 2 3 5 8 13 21 34 – 前 n 个斐波那契数（旧试卷中的题目）。
4. 错 – O(1)，只改变下标。
5. a) 栈，b) 队列，c) 优先队列。
-->

---

<!-- _class: fejezet -->
<!-- _footer: "" -->

# 实践课

---

<!-- _class: gyak -->

## 示例程序

| 程序 | Java | Go | 演示内容 |
|---|---|---|---|
| 朴素队列 | `NaiveQueue.java` | – | “漂移”队列的问题，用移动元素修正 |
| 循环队列 | `CircularQueue.java` | `w08/circularqueue` | 绕回，用计数器判断空/满 |
| 双端队列 | `CircularDeque.java` | `w08/deque` | 两端操作，回文检查 |
| 优先队列 | `PriorityQueueDemo.java` | `w08/priorityqueue` | 用无序数组和用堆实现 |

---

<!-- _class: gyak -->

## 练习

1. <strong>代码阅读：</strong>运行复习题 3 的代码，并解释输出！
2. <strong>约瑟夫问题：</strong><em>n</em> 个人围成一圈，每数到第 <em>k</em> 个人就淘汰。最后剩下谁？（用队列：把 k − 1 个人送到队尾，第 <em>k</em> 个人淘汰。）
3. <strong>时间片轮转：</strong>模拟调度！进程：P1 = 5,P2 = 2,P3 = 4 个时间单位，时间片 = 2。每个进程什么时候结束？
4. 借助栈<strong>把队列逆序</strong>。
5. <strong>代码阅读：</strong>字母 = 入队，`*` = 出队并输出：`D A T * A * S * T R * U C * T`（与栈的结果比较！）
6. <strong>滑动平均：</strong>每来一个新数据，用 O(1) 步输出最近 <em>k</em> 个数据的平均值！

参考答案：`Exercises08.java`、`w08/exercises`

---

<!-- _class: gyak -->

## 课后作业

1. 实现<strong>不用计数器</strong>、始终留一个空位的循环队列！大小为 <em>n</em> 的数组能放多少个元素？
2. 扩展循环队列，使它满时把元素复制到一个<strong>两倍大</strong>的数组中（动态循环队列）！注意绕回元素的顺序！
3. 用<strong>两个栈实现队列</strong>（见第 7 周的课后作业）！与循环队列比较！
4. <strong>收银台模拟：</strong>顾客随机到达，一个收银员每分钟服务一位顾客。在不同的到达率下，平均等待时间是多少？

<!--
解题思路：
1. 空：front = (rear + 1) mod n；满：front = (rear + 2) mod n → 至多 n − 1 个元素。
2. 新数组中 i = 0..count−1:new[i] = T[(front + i) mod n]；然后 front = 0,rear = count − 1。
4. 每分钟：根据随机数决定是否 enqueue（到达时间）；如果队列不空，dequeue，等待时间 = 当前时间 − 到达时间。
-->
