队列
一、课上练习
编程练习
二、知识总结
✨ 核心思想
队列是一种先进先出(FIFO, First In First Out)的数据结构,它允许在一端(通常称为"队尾"或"rear")添加元素,在另一端(通常称为"队首"或"front")移除元素。队列的这种行为使其在需要按顺序处理数据的场景中非常有用,比如在任务调度、打印任务管理、在任何需要按到达顺序处理项目的场合。
✨ 队列的基本操作
队列主要支持以下操作:
- enqueue:向队列的队尾添加一个元素。
- dequeue:从队列的队首移除一个元素。
- front:查看队列的队首元素。
- isEmpty:检查队列是否为空。
- size:返回队列中的元素数量。
✨ 实现方式
队列可以通过多种数据结构实现,包括但不限于:
- 数组:使用数组实现队列时,需要处理一个潜在的问题:队列可能"溢出"数组的一端。解决这个问题的一个方法是使用循环数组。
- 链表:使用链表实现队列可以提供动态的内存使用。通常使用链表的头部作为队头,使用链表的尾部作为队尾,这样可以轻松地添加和删除元素。
下面分别展示了三种队列的实现方式:
1#include <iostream>
2#include <stdexcept> // 用于抛出异常
3using namespace std;
4
5const int MAX_SIZE = 100; // 定义队列的最大容量
6
7int queue[MAX_SIZE]; // 队列数组
8int front = 0; // 队首索引
9int rear = 0; // 队尾索引
10int count = 0; // 队列中的元素数量
11
12// 入队操作
13void enqueue(int element) {
14 if (count >= MAX_SIZE) {
15 throw overflow_error("Queue overflow");
16 }
17 queue[rear] = element;
18 rear = (rear + 1) % MAX_SIZE;
19 count++;
20}
21
22// 出队操作
23int dequeue() {
24 if (count <= 0) {
25 throw underflow_error("Queue underflow");
26 }
27 int result = queue[front];
28 front = (front + 1) % MAX_SIZE;
29 count--;
30 return result;
31}
32
33// 查看队首元素
34int peek() {
35 if (count <= 0) {
36 throw runtime_error("Queue is empty");
37 }
38 return queue[front];
39}
40
41// 检查队列是否为空
42bool my_is_empty() {
43 return count == 0;
44}
45
46int main() {
47 try {
48 enqueue(10);
49 enqueue(20);
50 enqueue(30);
51
52 cout << "Front element is: " << peek() << endl; // 输出队首元素
53 cout << "Dequeue element: " << dequeue() << endl; // 出队并输出
54 cout << "After one dequeue, front element is: " << peek() << endl; // 再次输出队首元素
55
56 // 入队更多元素进行测试
57 enqueue(40);
58 enqueue(50);
59
60 // 继续出队
61 while (!my_is_empty()) {
62 cout << "Dequeue element: " << dequeue() << endl;
63 }
64 } catch (const exception& e) {
65 cerr << "Error: " << e.what() << endl;
66 }
67
68 return 0;
69}✨ 执行示例
下面通过一个具体示例,展示队列的 enqueue 和 dequeue 操作的完整执行过程。使用循环数组实现,数组大小 MAX_SIZE = 5。
操作序列: enqueue(10), enqueue(20), enqueue(30), dequeue(), dequeue(), enqueue(40), enqueue(50)
第 1 步:enqueue(10)
- queue[0] = 10,rear 从 0 变为 1,count = 1
- 队列状态(front=0, rear=1):[10, , , , ]
第 2 步:enqueue(20)
- queue[1] = 20,rear 从 1 变为 2,count = 2
- 队列状态(front=0, rear=2):[10, 20, , , _]
第 3 步:enqueue(30)
- queue[2] = 30,rear 从 2 变为 3,count = 3
- 队列状态(front=0, rear=3):[10, 20, 30, , ]
第 4 步:dequeue() -> 返回 10
- 取出 queue[0] = 10,front 从 0 变为 1,count = 2
- 队列状态(front=1, rear=3):[, 20, 30, , _]
第 5 步:dequeue() -> 返回 20
- 取出 queue[1] = 20,front 从 1 变为 2,count = 1
- 队列状态(front=2, rear=3):[, , 30, , ]
第 6 步:enqueue(40)
- queue[3] = 40,rear 从 3 变为 4,count = 2
- 队列状态(front=2, rear=4):[, , 30, 40, _]
第 7 步:enqueue(50)
- queue[4] = 50,rear 从 4 变为 0(循环!(4+1)%5=0),count = 3
- 队列状态(front=2, rear=0):[, , 30, 40, 50]
注意第 7 步中 rear 回到了数组开头,这就是循环数组的核心思想——利用取模运算使数组空间可以重复使用。
1#include<iostream>
2using namespace std;
3
4// 定义链表节点结构,用于实现队列
5struct Node {
6 int value; // 节点存储的值
7 Node *next; // 指向下一个节点的指针
8};
9
10// 函数向队列中推入一个新元素
11void push(Node *head, Node *&tail, int x) {
12 Node *node = new Node; // 创建新的节点
13 node->value = x; // 设置节点存储的值
14 node->next = NULL; // 新节点的下一个节点设置为NULL,因为它将成为队列的最后一个节点
15 if (tail) { // 如果队列非空
16 tail->next = node; // 将当前队尾节点的next指向新节点
17 } else { // 如果队列为空
18 head->next = node; // 将头节点的next指向新节点
19 }
20 tail = node; // 更新队尾指针指向新节点
21}
22
23// 函数从队列中弹出队首元素
24int pop(Node *head) {
25 Node *node = head->next; // 获取队首节点
26 if (node) { // 如果队列不为空
27 int value = node->value; // 暂存队首元素的值
28 head->next = node->next; // 将头节点的next指向队首节点的下一个节点,从而移除队首节点
29 delete node; // 释放队首节点的内存
30 return value; // 返回队首元素的值
31 }
32 return -1; // 如果队列为空,返回-1
33}
34
35int main() {
36 int n = 0; // 存储操作数量
37 cin >> n; // 读取操作数量
38 string op; // 存储操作类型(push或pop)
39 int x = 0; // 存储push操作的值
40 Node *head = new Node; // 创建头节点,用作队列的头部
41 head->next = NULL; // 初始化头节点的next为NULL
42 Node *tail = NULL; // 初始化队尾指针为NULL
43 for (int i = 0; i < n; i++) { // 遍历所有操作
44 cin >> op; // 读取操作类型
45 if (op == "push") { // 如果是push操作
46 cin >> x; // 读取要push的值
47 push(head, tail, x); // 执行push操作
48 } else { // 如果是pop操作
49 int value = pop(head); // 执行pop操作,并获取被移除的队首元素的值
50 cout << value << endl; // 输出被移除的队首元素的值
51 }
52 }
53 return 0; // 程序结束
54}1#include <iostream>
2#include <queue>
3
4int main() {
5 queue<int> q;
6
7 // 向队列中添加元素
8 q.push(10);
9 q.push(20);
10 q.push(30);
11
12 // 输出队列前端的元素
13 cout << "Front element is: " << q.front() << endl;
14
15 // 移除队列前端的元素
16 q.pop();
17 cout << "Front element after one pop: " << q.front() << endl;
18
19 // 检查队列是否为空
20 if (!q.empty()) {
21 cout << "Queue is not empty" << endl;
22 }
23
24 // 输出队列的大小
25 cout << "Queue size is: " << q.size() << endl;
26
27 return 0;
28}✨ 应用场景
队列在各种计算机科学领域都非常重要,常见的使用场景包括:
- 事件处理系统:管理事件或消息的顺序处理。
- 资源共享:如打印机或 CPU 调度,确保资源的公平使用。
- 数据缓冲:例如在多线程数据处理或网络通信中缓冲数据。
- 宽度优先搜索(BFS)算法:在图搜索算法中,队列用于持续追踪待访问的节点。
✨ 解题步骤详解
以"买票需要的时间"为例,介绍如何使用队列解决问题:
问题: n 个人排队买票,第 i 个人买票需要 t[i] 秒。求第 k 个人买完票的总等待时间。每个人每次只能买 1 秒的票,如果还需要更多时间则回到队尾重新排队。
解题步骤:
- 初始化队列:将所有人的编号(0 到 n-1)依次入队。
- 模拟过程:
- 每次从队首取出一个人,将其所需时间减 1,总时间加 1。
- 如果该人的剩余时间 > 0,将其放回队尾。
- 如果该人的剩余时间 = 0 且编号等于 k,返回当前总时间。
- 循环直到找到答案。
关键点: 队列的 FIFO 特性天然模拟了排队的过程,每个人按到达顺序服务,服务完如果还需要则回到队尾。
✨ 常见错误
- 循环数组中取模运算遗漏:front 和 rear 移动时必须对 MAXSIZE 取模,否则会数组越界。正确写法:
rear = (rear + 1) % MAXSIZE。 - 队列满和队列空的判断混淆:在循环数组中,如果只用 front == rear 判断,无法区分队列满和队列空。解决方法是额外维护一个 count 变量,或者浪费一个数组位置。
- 链表实现时忘记更新 tail 指针:当队列只剩一个元素并 dequeue 后,tail 指针仍然指向已删除的节点,必须将 tail 置为 NULL。
- 使用 STL queue 时 pop() 前未检查 empty():对空队列调用 front() 或 pop() 是未定义行为。
- 混淆栈和队列的操作顺序:栈是后进先出(LIFO),队列是先进先出(FIFO),在选择数据结构时要根据问题需求判断。
三、课后练习
编程练习
- 周末舞会:L3213