前言:
在上期博文中我带大家对stack进行深入的学习,本期我将带领学习的是关于 queue的基本知识,并且还将给大家介绍并实现 priority_queue。接下来,让我们正式本期的内容。
(一)queue的基本介绍
- 文档链接:queue的文档介绍
💨 queue是一个容器适配器,它是基于deque或者list实现的。queue的特点是先进先出(FIFO)
底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
- empty:检测队列是否为空
- size:返回队列中有效元素的个数
- front:返回队头元素的引用
- back:返回队尾元素的引用
- push_back:在队列尾部入队列
- 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的文档介绍
priority_queue是一个容器适配器,它是基于vector实现的,默认情况下是按照元素大小排序的(大根堆)。可以通过指定比较函数改变排序方式。
底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭 代器访问,并支持以下操作:
- push():元素入队
- pop():元素出队
- top():返回堆顶元素
- empty():判断堆是否为空
- 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的简单介绍(了解)
- 文档链接:deque文档介绍
1、deque的原理介绍
deque(双端队列)
- 是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1);
- 与vector比较,头插效率高,不需要搬移元素;
- 与list比较,空间利用率比较高。
deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:
双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:
那deque是如何借助其迭代器维护其假想连续的结构呢?
- deque是一个由多个固定大小的缓冲区构成的序列容器,每个缓冲区都被当作一个元素来处理。为了维护deque的假想连续结构,deque的迭代器实际上是一种复合迭代器,它将多个子迭代器封装在一起,以提供对整个deque的访问。
- 具体来说,deque的迭代器实际上就是一个指向数据缓冲区的指针数组,每个指针指向一个单独的存储块,而这些存储块构成了deque的数据缓冲区。
- deque会通过维护多个内部的缓冲区来实现假想连续的结构,并且迭代器也会对这些内部缓冲区进行适当划分和处理;
- 在deque内部,每个缓冲区被称作一个节点node,并在其内部维护了一个完整的数据块;
- 当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; }
解释说明:
- 在上述代码中,我们使用deque容器和迭代器来存储和访问一系列整数;
- 首先,我们定义了一个deque对象d,并使用begin()函数获取其头部的迭代器;
- 然后,我们使用迭代器遍历deque中的元素,并输出它们的值;
- 接下来,我们调用push_front()函数和push_back()函数在deque的头部和尾部插入两个新元素;
- 最后,我们再次使用迭代器遍历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的优势
- deque提供了在两端进行快速插入和删除的操作,并且支持随机访问。这使得它可以高效地实现栈和队列的基本操作,例如压入元素、弹出元素、获取栈顶或队头元素等等。
- 其次,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的底层默认容器的具体原因。
以上便是本文的全部内容,希望对大家有所帮助,感谢各位的支持与观看!!!