各类资料学习下载合集
https://pan.quark.cn/s/8c91ccb5a474
今天,我们来聊一个非常基础且重要的数据结构——队列(Queue)。如果你理解了生活中的“排队”,那么恭喜你,你已经掌握了队列一半的精髓!这篇博客将带你深入队列的世界,特别是如何用我们最熟悉的数组来实现它。
一、再谈队列:为什么是“先进先出”?
队列的核心:先进先出(First-In, First-Out, FIFO)。
想象一下在食堂排队打饭的场景:
- 你是第一个排队的,那你就是第一个打到饭的。
- 后来的人只能排在你后面,等前面的人都打完饭了才轮到他们。
这个过程完美地诠释了 FIFO 原则。在编程中,我们经常需要处理类似的任务流,比如打印机任务、网络请求包、消息处理等,这些场景都要求按顺序、公平地处理,因此队列就成了不二之选。
二、用数组实现队列:一个直观但有“坑”的想法
用数组(顺序存储)来实现队列,是最符合直觉的方案。我们可以规定:
- 数组的
0 索引端作为队头(Front):负责出数据。 - 数组的末尾端作为队尾(Rear):负责进数据。
听起来很简单,对吧?我们来分析一下操作:
- 入队 (Push): 在数组末尾添加一个元素。这个操作很简单,时间复杂度是 O(1)。
- 出队 (Pop): 移除数组索引为
0 的元素。问题来了! 当我们移除第一个元素后,数组中会留下一个空位。为了保持队列的连续性,我们必须将后面所有的元素(从索引 1 到 n-1)都向前移动一位。
这个“移动所有元素”的操作,其时间复杂度是 O(n),其中 n 是队列中元素的数量。如果队列非常大,每次出队都可能导致大量的元素移动,效率极低。这是使用普通数组模拟队列的核心痛点。
三、更优方案:巧用“动态数组”封装
笔记中提到了一个绝佳的思路:利用之前封装好的动态数组来实现队列。
这里的“动态数组”可以理解为 C++ 中的 std::vector、Java 中的 ArrayList 或我们自己实现的能够自动扩容并提供便捷操作接口的数组结构。它的好处在于:
- 封装了复杂操作:我们不需要手动管理内存和元素移动,动态数组为我们提供了高级接口,如
insert、erase 等。 - 简化队列实现:我们可以将队列的入队和出队操作,直接映射到动态数组的“尾部插入”和“头部删除”操作上。
这样,我们就可以专注于队列本身的逻辑,而不是陷入繁琐的数组操作中。
四、代码实战:从零构建一个基于数组的队列
接下来,让我们动手用 C++ 来实现一个基于 std::vector(C++ 的动态数组)的队列。我们将遵循笔记中提到的接口设计。
1. 定义测试数据结构
为了让测试更有趣,我们不使用简单的 int,而是使用一个 Person 结构体。
#include <iostream>
#include <string>
#include <vector>
// 定义一个 Person 结构体用于测试
struct Person {
std::string name;
int age;
};
2. 设计并实现队列类 ArrayQueue
我们将所有必要的接口都实现为这个类的方法。
// 基于动态数组(vector)实现的队列类
class ArrayQueue {
private:
// 使用 std::vector 作为底层容器
std::vector<Person> data;
public:
// 构造函数 (隐式默认即可,无需特殊初始化)
ArrayQueue() {
std::cout << "队列已成功初始化!" << std::endl;
}
// 析构函数 (vector 会自动管理内存,无需手动销毁)
~ArrayQueue() {
std::cout << "队列已成功销毁!" << std::endl;
}
// 1. 入队操作 (在队尾添加)
void push(const Person& p) {
data.push_back(p);
}
// 2. 出队操作 (从队头移除)
void pop() {
if (empty()) {
std::cout << "队列为空,无法出队!" << std::endl;
return;
}
// vector::erase(begin()) 会移除第一个元素并自动前移后续元素
data.erase(data.begin());
}
// 3. 返回队头元素
Person& front() {
if (empty()) {
// 在实际工程中,这里应该抛出异常
throw std::runtime_error("无法访问空队列的队头!");
}
return data.front(); // 或者 data[0]
}
// 4. 返回队尾元素
Person& back() {
if (empty()) {
// 在实际工程中,这里应该抛出异常
throw std::runtime_error("无法访问空队列的队尾!");
}
return data.back(); // 或者 data[data.size() - 1]
}
// 5. 返回队列大小
int size() const {
return data.size();
}
// 6. 判断队列是否为空
bool empty() const {
return data.empty();
}
};
五、完整测试与结果分析
现在,我们来编写 main 函数,模拟课堂笔记中的测试流程,看看我们的队列是否工作正常。
int main() {
// 1. 初始化队列
ArrayQueue queue;
std::cout << "----------------------------------------" << std::endl;
// 2. 准备数据并入队
std::cout << "开始入队..." << std::endl;
Person p1 = {"张三", 20};
Person p2 = {"李四", 22};
Person p3 = {"王五", 19};
Person p4 = {"赵六", 25};
queue.push(p1);
std::cout << p1.name << " (age " << p1.age << ") 已入队。" << std::endl;
queue.push(p2);
std::cout << p2.name << " (age " << p2.age << ") 已入队。" << std::endl;
queue.push(p3);
std::cout << p3.name << " (age " << p3.age << ") 已入队。" << std::endl;
queue.push(p4);
std::cout << p4.name << " (age " << p4.age << ") 已入队。" << std::endl;
std::cout << "----------------------------------------" << std::endl;
// 3. 打印队列当前状态
std::cout << "当前队列大小: " << queue.size() << std::endl;
if (!queue.empty()) {
std::cout << "队头元素是: " << queue.front().name << std::endl;
std::cout << "队尾元素是: " << queue.back().name << std::endl;
}
std::cout << "----------------------------------------" << std::endl;
// 4. 循环出队,验证 FIFO
std::cout << "开始按顺序出队..." << std::endl;
while (!queue.empty()) {
// 获取队头元素
Person current_person = queue.front();
std::cout << "服务中: " << current_person.name << " (age " << current_person.age << ")" << std::endl;
// 执行出队
queue.pop();
std::cout << current_person.name << " 已出队。剩余人数: " << queue.size() << std::endl << std::endl;
}
std::cout << "----------------------------------------" << std::endl;
// 5. 检查最终状态
std::cout << "所有人都已出队,当前队列是否为空? " << (queue.empty() ? "是" : "否") << std::endl;
std::cout << "----------------------------------------" << std::endl;
// main函数结束时,queue对象会被销毁,自动调用析构函数
return 0;
}
运行结果:
队列已成功初始化!
----------------------------------------
开始入队...
张三 (age 20) 已入队。
李四 (age 22) 已入队。
王五 (age 19) 已入队。
赵六 (age 25) 已入队。
----------------------------------------
当前队列大小: 4
队头元素是: 张三
队尾元素是: 赵六
----------------------------------------
开始按顺序出队...
服务中: 张三 (age 20)
张三 已出队。剩余人数: 3
服务中: 李四 (age 22)
李四 已出队。剩余人数: 2
服务中: 王五 (age 19)
王五 已出队。剩余人数: 1
服务中: 赵六 (age 25)
赵六 已出队。剩余人数: 0
----------------------------------------
所有人都已出队,当前队列是否为空? 是
----------------------------------------
队列已成功销毁!
结果分析: 从输出结果可以清晰地看到:
- 入队顺序是:张三 -> 李四 -> 王五 -> 赵六。
- 出队(服务)顺序也是:张三 -> 李四 -> 王五 -> 赵六。
-
front() 总是返回最早入队的元素,back() 总是返回最晚入队的元素。
这完全符合“先进先出”的原则,证明我们的数组队列实现是正确的!
六、总结
今天,我们深入探讨了如何使用数组来实现队列。我们得出了几个关键结论:
- 队列是严格遵循 FIFO 原则的数据结构。
- 直接使用普通数组实现队列,在出队时会因元素移动而导致 O(n) 的时间复杂度,效率较低。
- 通过封装动态数组(如
std::vector),我们可以大大简化实现过程,将复杂的底层操作交给容器来处理。 - 一个完备的队列应提供
push, pop, front, back, size, empty 等核心接口。
虽然这种基于动态数组的实现非常方便,但其出队操作的效率问题依然存在(vector::erase 内部仍然需要移动元素)。在对性能有极致要求的场景下,更高效的实现方式是循环队列(Circular Queue),它通过巧妙的指针移动来避免数据搬迁,能将出队操作的平均时间复杂度也优化到 O(1)。

500

被折叠的 条评论
为什么被折叠?



