• 数据结构与算法分析】0基础带你学数据结构与算法分析03--队列 (Queue)


    前言

    Queue 也是一种受限的线性结构,其末尾被称为队尾 (rear),而头部被称为队首 (front)。向队列中添加元素被称为 入队 (enqueue),enqueue 只能在队尾操作;从队列中移除元素被称为 出队 (dequeue),dequeue 只能在队首操作。因此这种数据结构也被称为 先进先出 (First-In First-Out, FIFO)。

     Queue ADT

    1. template <class T>
    2. concept queue =
    3. requires(T a, const T& b, const typename T::value_type& value) {
    4. requires swappable;
    5. requires erasable<typename T::value_type>;
    6. requires same<typename T::reference, typename T::value_type&>;
    7. requires same<typename T::const_reference, const typename T::value_type&>;
    8. requires unsigned<typename T::size_type>;
    9. { a.empty() } -> boolean;
    10. { a.size() } -> typename T::size_type;
    11. { a.front() } -> typename T::reference;
    12. { b.front() } -> typename T::const_reference;
    13. { a.back() } -> typename T::reference;
    14. { b.back() } -> typename T::const_reference;
    15. a.push(value);
    16. a.pop();
    17. };

    队列的顺序实现

    队列本质上是受限的线性表,因此其与 stack 一样可以直接在线性表上做 adaptor,方便快速的实现。但是对于顺序实现的线性表来说,在队首操作时间复杂度为 O(N) ,其代价太高。我们需要优化现有结构,让其操作时间复杂度降为 O(1) 。

    循环队列

    对线性表的顺序实现进行简单的改进,使用两个指针 startfinish 指向队首元素与队尾元素,而数组边界使用 beginend 指示。插入元素时使 finish+1 ,删除时使 start+1 。但是当 finish 到达数组边界时,就会发生问题,无论 start 前是否剩余空位,都不能再添加元素,因为 finish 已到达边界。这种情况被称为 假溢出

    显然这个小改进并不能满足需求,为了正常使用,我们假设这个数组是头尾相接的循环数组。因此逻辑上的循环数组不用担心假溢出问题,但也需要每次插入、移除元素时需要检查指针是否到达数组边界,如果已在边界则移动到数组的另一边。

    现在思考一下真溢出问题,数组被完完全全的填满了,没有可以容纳元素的方法。这样我们不得不申请更大的一块数组,并将其中元素完整复制进去。当生长时需要 O(N) 的时间复杂度完成迁移,并且需要完全按照从 front 到 rear 的顺序进行。 

    分块的双端队列

    对于循环队列的缺点进行改进,我们将使用一个全新的方式实现顺序存储。具体思路是:将多个相同大小的块数组组合起来,元素可以被存放在多个不连续的块上,但其连续存储。使用两个指针 start 与 finish 分别指向队首元素与队尾元素,对于每个块有单独的指针指向其头结点。

    可以看到,由多个相同大小的块组成了整个存储结构,并且元素在其中顺序存储。可以发现有些块指针并没有引用块,在我们需要的时候,我们可以为其请求一个块,这样我们的数据可以持续的向两边生长,而不需要在生长重新拷贝整个结构。

    由于其是多个块数组实现的,且元素顺序、连续排列,因此其可以实现 随机访问 ,其迭代器类型为 radom_access_iterator 。至于跨块访问,应该由实现者对其处理,对使用者透明,使用时可以将其逻辑上作为一个大的块。

    可以高效的在两端进行插入、移除元素,但由于分块的特性,需要由实现隐藏其底层块。

     分块双端队列的实现

    由于分块双端队列的复杂性,我们将详细说明一下其实现细节。

    分块双端队列的迭代器

    由于迭代器肩负着隐藏底层块结构的作用,并且还要支持随机访问数据。因此迭代器的实现很重要。

    1. template <class Element>
    2. struct iterator {
    3. Element* cur; // 迭代器指向的元素
    4. Element* first; // 当前元素所在块数组的起始指针
    5. Element* last; // 当前元素所在块数组的末尾指针
    6. Element** node; // 当前元素所在的块
    7. };

    为了进行随机访问,必须确定当前元素所在的块,才能在不同块之间进行随机访问。在进行随机访问的示例中, chunk_capacity 是一个获取每个块数组可以容纳有多少元素的函数,因此确认每个块的末尾边界。而 __set_node 根据 it 当前指向的元素与将步进的块数量,来设置随机访问的目标结点正确的块信息。

    1. template <class Element>
    2. void __set_node(iterator& it, const difference_type& n) {
    3. it.node += n;
    4. it.first = *it.node;
    5. it.last = *it.node + chunk_capacity();
    6. }
    7. template <class Element>
    8. iterator& operator+=(iterator& it, const difference_type& n) {
    9. difference_type cap = chunk_capacity();
    10. const difference_type offset = n + (it.cur - it.first);
    11. if (0 <= offset && offset < chunk_cap) {
    12. it.cur += n;
    13. } else {
    14. const difference_type tmp = offset < 0 ? -((-offset - 1) / cap) - 1 : offset / cap;
    15. __set_node(it, tmp);
    16. it.cur = it.first + (offset - tmp * cap);
    17. }
    18. return it;
    19. }

    视线放在 operator+= 这个函数,offset 用于判断当前结点需要向前或后步进多少个元素,加 it.cur−it.first是为了将相对起点从 cur 移动到当前所在块的开始位置 first。如果 0≤offset≤capacity 则意味着这次随机访问并不会变更所在块,否则需要计算变更到哪一块。仔细判断步进方向与步进大小,向前步进时将移动 (−offset−1)/cap −1 个块,而向后步进时则需要移动 offset/cap​ 个块。最终元素的位置将相对新的起始指针 offset−tmp∗cap 个元素。

    这是相对复杂的步进,而其他步进与此差不了多少,就不再举例说明。

    分块双端队列的存储结构

    在其存储结构中,需要有一个指针指向块指针的数组的首元素,简单的说就是指向指针的指针,没有使用的块指针应该置空 (nullptr 或 NULL)。其中还需要两个 iterator 分别指向队首元素与队尾元素。

    1. template <class Element>
    2. struct Base {
    3. using iterator = iterator;
    4. size_t size;
    5. Element** chunks;
    6. iterator begin;
    7. iterator end;
    8. };

    尾言

    好啦,到这里我们队列的基础知识也就结束了,希望大家都可以理解运用队列‘下期我们向大家介绍串的知识。

  • 相关阅读:
    Linux中socket地址API
    【NodeJs-5天学习】第四天存储篇③ ——基于物联网的WiFi自动打卡考勤系统,升级存储为mysql,提醒功能改为QQ
    14. 几种 SAP ABAP OData 服务的性能评估和测试工具介绍
    PHP 在 Microsoft Windows 下的命令行方式
    e=vm2:vm in evm
    Android桌面Launcher源码浅析
    PixiJS源码分析系列:第二章 渲染在哪里开始?
    五年Python从业者,谈谈Python的一些优缺点
    ICC2: secondary pg pin的作用与连接
    【国外框架】—— quasar 应用程序插件
  • 原文地址:https://blog.csdn.net/qq_62464995/article/details/127442084