深入浅出数据结构:用数组玩转队列 (Queue)——从理论到实践

各类资料学习下载合集  

​​​​​​https://pan.quark.cn/s/8c91ccb5a474​

今天,我们来聊一个非常基础且重要的数据结构——队列(Queue)。如果你理解了生活中的“排队”,那么恭喜你,你已经掌握了队列一半的精髓!这篇博客将带你深入队列的世界,特别是如何用我们最熟悉的数组来实现它。

一、再谈队列:为什么是“先进先出”?

队列的核心:先进先出(First-In, First-Out, FIFO)

想象一下在食堂排队打饭的场景:

  • 你是第一个排队的,那你就是第一个打到饭的。
  • 后来的人只能排在你后面,等前面的人都打完饭了才轮到他们。

这个过程完美地诠释了 FIFO 原则。在编程中,我们经常需要处理类似的任务流,比如打印机任务、网络请求包、消息处理等,这些场景都要求按顺序、公平地处理,因此队列就成了不二之选。

二、用数组实现队列:一个直观但有“坑”的想法

用数组(顺序存储)来实现队列,是最符合直觉的方案。我们可以规定:

  • 数组的 0​ 索引端作为队头(Front):负责出数据。
  • 数组的末尾端作为队尾(Rear):负责进数据。

听起来很简单,对吧?我们来分析一下操作:

  1. 入队 (Push): 在数组末尾添加一个元素。这个操作很简单,时间复杂度是 O(1)。
  2. 出队 (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()​​ 总是返回最晚入队的元素。

这完全符合“先进先出”的原则,证明我们的数组队列实现是正确的!

六、总结

今天,我们深入探讨了如何使用数组来实现队列。我们得出了几个关键结论:

  1. 队列是严格遵循 FIFO 原则的数据结构。
  2. 直接使用普通数组实现队列,在出队时会因元素移动而导致 O(n) 的时间复杂度,效率较低。
  3. 通过封装动态数组(如 std::vector​),我们可以大大简化实现过程,将复杂的底层操作交给容器来处理。
  4. 一个完备的队列应提供 ​​push​​, ​​pop​​, ​​front​​, ​​back​​, ​​size​​, ​​empty​​ 等核心接口。

虽然这种基于动态数组的实现非常方便,但其出队操作的效率问题依然存在(​​vector::erase​​ 内部仍然需要移动元素)。在对性能有极致要求的场景下,更高效的实现方式是循环队列(Circular Queue),它通过巧妙的指针移动来避免数据搬迁,能将出队操作的平均时间复杂度也优化到 O(1)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

web安全工具库

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

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

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

打赏作者

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

抵扣说明:

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

余额充值