【C++】容器篇(四)—— queue的基本介绍以及模拟实现

简介: 【C++】容器篇(四)—— queue的基本介绍以及模拟实现

前言:

在上期博文中我带大家对stack进行深入的学习,本期我将带领学习的是关于 queue的基本知识,并且还将给大家介绍并实现  priority_queue。接下来,让我们正式本期的内容。


(一)queue的基本介绍

💨  queue是一个容器适配器,它是基于deque或者list实现的。queue的特点是先进先出(FIFO)

底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:

  1. empty:检测队列是否为空
  2. size:返回队列中有效元素的个数
  3. front:返回队头元素的引用
  4. back:返回队尾元素的引用
  5. push_back:在队列尾部入队列
  6. pop_front:在队列头部出队列

标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque


 

(二)基本使用

  • queue的主要的接口函数大概就是以下这几个,我们也将围绕这几个接口来模拟实现。


(三)模拟实现

  • 代码如下:
#pragma once
namespace zp
{
  template<class T, class Container = deque<T>>
  class queue
  {
  public:
    void push(const T& x)
    {
      return _qq.push_back(x);
    }
    void pop()
    {
      if (!_qq.empty())
      {
        return _qq.pop_front();
      }
    }
    const T& front()
    {
      return _qq.front();
    }
    const T& back()
    {
      return _qq.back();
    }
    bool empty()
    {
      return _qq.empty();
    }
    size_t size()
    {
      return _qq.size();
    }
  private:
    Container _qq;
  };
  void test_queue_1()
  {
    queue<int> q;
    //queue<int, list<int>>q;
    //queue<int, vector<int>>q;
    q.push(1);
    q.push(2);
    q.push(3);
    q.push(4);
    q.push(5);
    //队首元素
    if ((!q.empty()))
    {
      cout << q.front() << " "<<endl;
    }
  
    q.pop();
    cout << q.front() << endl;
    // 检查队列是否为空
    if (q.empty())
    {
      cout << "Stack is empty" << endl;
    }
    else
    {
      cout << "queue is not empty" << endl;
    }
    // 获取队列的大小
    cout << "queue size is " << q.size() << endl;
    //输出队列元素
    while (!q.empty())
    {
      cout << q.front() << " ";
      q.pop();
    }
    cout << endl;
  }
}

 

(四)priority_queue的基本介绍

priority_queue是一个容器适配器,它是基于vector实现的,默认情况下是按照元素大小排序的(大根堆)。可以通过指定比较函数改变排序方式。

底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭 代器访问,并支持以下操作:

  1. push():元素入队
  2. pop():元素出队
  3. top():返回堆顶元素
  4. empty():判断堆是否为空
  5. size():返回堆中元素数量

💨  大家如果想了解更多,可以借助文档仔细琢磨!!!


(五)基本使用

  • 优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成 堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。

基本使用跟上面的queue是大同小异的,我们直接模拟实现即可。

⚔️  注意: 默认情况下priority_queue是大堆。⚔️


(六)priority_queue的模拟实现

  • 代码如下:
#pragma once
namespace zp
{
    template<class T, class Compare = less<T>>
    class priority_queue 
    {
    public:
        void push(const T& val) 
        {
            _qq.push_back(val);
            push_heap(_qq.begin(), _qq.end(), _cmp);
        }
        void pop() 
        {
           pop_heap(_qq.begin(), _qq.end(), _cmp);
           _qq.pop_back();
        }
        const T& top() const 
        {
            return _qq.front();
        }
        bool empty() const 
        {
            return _qq.empty();
        }
        size_t size() const 
        {
            return _qq.size();
        }
    private:
        vector<T> _qq;
        Compare _cmp;
    };
    void test_priority_queue()
    {
        priority_queue<int> pq;
        pq.push(1);
        pq.push(3);
        pq.push(2);
        pq.push(5);
        pq.push(4);
        while (!pq.empty())
        {
            cout << pq.top() << " ";
            pq.pop();
        }
        cout << endl;
    }
}

(六)deque的简单介绍(了解)

1、deque的原理介绍

deque(双端队列)

  • 是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1);
  • 与vector比较,头插效率高,不需要搬移元素;
  • 与list比较,空间利用率比较高。

deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:

 

双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:


那deque是如何借助其迭代器维护其假想连续的结构呢?

  1. deque是一个由多个固定大小的缓冲区构成的序列容器,每个缓冲区都被当作一个元素来处理。为了维护deque的假想连续结构,deque的迭代器实际上是一种复合迭代器,它将多个子迭代器封装在一起,以提供对整个deque的访问。
  2. 具体来说,deque的迭代器实际上就是一个指向数据缓冲区的指针数组,每个指针指向一个单独的存储块,而这些存储块构成了deque的数据缓冲区。

 

  1. deque会通过维护多个内部的缓冲区来实现假想连续的结构,并且迭代器也会对这些内部缓冲区进行适当划分和处理;
  2. 在deque内部,每个缓冲区被称作一个节点node,并在其内部维护了一个完整的数据块;
  3. 当deque在两端插入/删除元素时,它会动态重新分配节点,使得每个节点上的数据块都可以被利用,从而保证数据块是假想连续的。同时,迭代器会利用这些节点间的相对偏移量来实现对deque的高效遍历。

下面是一个简单的示例程序,演示了如何使用deque迭代器来访问deque中的元素:

#include <iostream>
#include <deque>
using namespace std;
int main() 
{
   deque<int> d = { 0,1, 2, 3, 4 };
   deque<int>::iterator it = d.begin();
    // 使用迭代器访问deque
    for (; it != d.end(); ++it) {
        cout << *it << " ";
    }
   cout <<endl;
    // 在头部和尾部插入元素
    d.push_front(6);
    d.push_back(5);
    // 再次使用迭代器访问deque
    for (it = d.begin(); it != d.end(); ++it) {
        cout << *it << " ";
    }
   cout << endl;
    return 0;
}

解释说明:

  1. 在上述代码中,我们使用deque容器和迭代器来存储和访问一系列整数;
  2. 首先,我们定义了一个deque对象d,并使用begin()函数获取其头部的迭代器;
  3. 然后,我们使用迭代器遍历deque中的元素,并输出它们的值;
  4. 接下来,我们调用push_front()函数和push_back()函数在deque的头部和尾部插入两个新元素;
  5. 最后,我们再次使用迭代器遍历deque中的元素,并输出它们的值;

💨 可以看到,即使我们在deque中插入了新元素,迭代器仍然可以正确地指向各个元素,这得益于deque迭代器的复合结构和灵活的移动方式。

输出结果如下:


 

2、deque的缺陷

与vector比较,deque的优势是:

  • 头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是比vector高的。

与list比较,deque的优势是:

  • 其底层是连续空间,空间利用率比较高,不需要存储额外字段。

deque有一个致命缺陷:

  • 不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历;
  • 因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作 为stack和queue的底层数据结构。

(七)deque作为stack和queue的底层默认容器

1、分析

💨 stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器,比如vector和list都可以;

💨 queue是先进先出的特殊线性数据结构,只要具有 push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如list。

2、deque的优势

  1. deque提供了在两端进行快速插入和删除的操作,并且支持随机访问。这使得它可以高效地实现栈和队列的基本操作,例如压入元素、弹出元素、获取栈顶或队头元素等等。
  2. 其次,deque的内存分配策略相对于vector更加灵活。vector的内存分配策略是连续的,在需要重新分配内存时,所有的元素都需要被复制到新的内存空间中;而deque的内存分配策略是分块的,每个元素所占用的内存空间是独立的,可以在不影响整个容器的情况下单独分配和回收。

💨 这样,当需要重新分配内存时,只需要重新分配一部分内存空间,而不需要把所有的元素都复制到新的内存空间中。这使得deque相对于vector具有更好的动态扩展性能和更低的内存开销。

3、原因

STL中对stack和 queue默认选择deque作为其底层容器,主要是因为:

  • 1. stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
  • 2. 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。

因此综上所述,由于deque提供了高效的双端操作和灵活的内存分配策略,使得它成为了实现stack和queue的底层默认容器的理想选择,结合了deque的优点,而完美的避开了其缺陷。


总结

到此,本文的内容便到此结束了,接下来我们简单的回顾一下本文都讲了什么吧!!!

  • 1、首先,我们介绍有关队列的有关知识,知道了其FIFO的特性,并手动实现了一个queue;
  • 2、其次,我们认识了priority_queue,跟学习queue一样,我们知道了在默认情况下队列中的元素是按照大根堆的序列排的;
  • 3、紧接着,我带领大家简单的认识一下关于deque,了解了相关概念以及特性;
  • 4、最后,我们整理了deque作为stack和queue的底层默认容器的具体原因。

以上便是本文的全部内容,希望对大家有所帮助,感谢各位的支持与观看!!!

相关文章
|
2天前
|
C++ 索引 容器
黑马c++ STL部分 笔记(9) map/multimap容器
黑马c++ STL部分 笔记(9) map/multimap容器
|
2天前
|
C++ 容器
黑马c++ STL部分 笔记(8) set/ multiset 容器
黑马c++ STL部分 笔记(8) set/ multiset 容器
|
2天前
|
存储 C++ 容器
黑马c++ STL部分 笔记(7) list容器
黑马c++ STL部分 笔记(7) list容器
|
2天前
|
C++ 容器
黑马c++ STL部分 笔记(6) queue 容器
黑马c++ STL部分 笔记(6) queue 容器
|
2天前
|
C++ 容器
黑马c++ STL部分 笔记(5) stack容器
黑马c++ STL部分 笔记(5) stack容器
|
2天前
|
C++ 容器
黑马c++ STL部分 笔记(3) deque容器
黑马c++ STL部分 笔记(3) deque容器
|
2天前
|
C++ 容器
黑马c++ STL部分 笔记(3) vector容器
黑马c++ STL部分 笔记(3) vector容器
|
2天前
|
C++ 容器
黑马c++ STL部分 笔记(2) string容器
黑马c++ STL部分 笔记(2) string容器
|
2天前
|
C++ 容器
黑马c++ STL部分 笔记(1) vector容器
黑马c++ STL部分 笔记(1) vector容器
|
3天前
|
C++
C++派生类
C++派生类
13 0