目录
为什么选择deque作为stack和queue的底层默认容器
一、stack的介绍和使用
stack的介绍
stack的文档介绍:![]()
翻译:1.stack是一种 容器适配器 ,专门用在具有 后进先出(LIFO) 操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。2. stack是作为容器适配器被实现的, 容器适配器 即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层,元素特定容器的尾部(即栈顶)被压入和弹出。3. stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
- empty:判空操作
- size:获取有效元素个数操作
- back:获取尾部元素操作
- push_back:尾部插入元素操作
- pop_back:尾部删除元素操作
4、标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器, 默认情况下使用deque。
![]()
stack的使用
stack接口:
stack的使用非常简单,这里只做简单的实现演示,我们重点关注stack的模拟实现。
stack具有后进先出(LIFO)的特性。
模拟实现stack
我们要模拟实现一个stack(LIFO后进先出)栈可以怎样实现呢?
从stack的接口可以看出要模拟一个stack使用vector、list都完全可以实现,因为vector、list都具有以下操作可以用来实现stack的接口。
- empty:判空操作
- size :获取有效元素个数操作
- back:获取尾部元素操作
- push_back:尾部插入元素操作
- pop_back:尾部删除元素操作
所以要模拟一个stack完全可以通过其他容器的转换来实现。
stack模拟实现:
说明:
在模板参数列表中我们新增了一个参数Container类型,是一个容器类,接收的是一个容器。stack的接口都是通过调用容器_con的相关接口来实现的。
_con既可以是vector,也可以是list,取决于传的是什么容器,只要这个容器能实现stack的相关接口就都可以。
测试:
通过stack的模拟实现,更清晰的理解了stack及“stack是一种容器适配器”。
二、queue的介绍和使用
queue的介绍
queue文档介绍:
翻译:
1、队列是一种 容器适配器 ,专门用在具有 先进先出(FIFO) 操作的上下文环境中,其中从容器一端插入元素,另一端提取元素。2.、队列作为容器适配器实现, 容器适配器 即将特定容器类封装作为其底层容器类,queue提供一组特定的 成员函数来访问其元素。 元素从队尾入队列,从队头出队列。3、 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
- empty:检测队列是否为空
- size:返回队列中有效元素的个数
- front:返回队头元素的引用
- back:返回队尾元素的引用
- push_back:在队列尾部入队列
- pop_front:在队列头部出队列
4. 标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类, 则使用标准容器deque。![]()
queue的使用
queue接口:
queue的使用非常简单,这里只做简单的实现演示,我们重点关注queue的模拟实现。
queue具有先进先出(FILO)的特性。
模拟实现queue
我们要实现一个queue(FIFO先进先出)队列可以怎样实现呢?
从queue的接口可以看出,使用list同样可以实现,因为list具有以下操作可以用来实现queue的接口:
- empty:检测队列是否为空
- size:返回队列中有效元素的个数
- front:返回队头元素的引用
- back:返回队尾元素的引用
- push_back:在队列尾部入队列
- pop_front:在队列头部出队列
可以使用vector吗?
不可以,因为queue的接口中存在头删和尾插操作,使用vector来封装效率太低。vector没有头删操作,因为头删需要挪动数据效率太低,尾插又涉及到增容,代价太大了。
所以要模拟一个queue也同样完全可以使用list容器进行转换来实现。
queue模拟实现:
与stack的模拟实现类似,queue的接口都是通过调用容器_con的相关操作来实现的。
此时_con是一个list。
通过queue的模拟实现,更清晰的理解了queue及"queue是一个容器适配器"。
补充:stack和queue都不支持迭代器,因为支持了迭代器就支持了随机访问,就违背了stack后进先出和queue先进先出的特性了。
总结:虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和队列只是对其他容器的接口进行了包装。
三、STL标准库中stack和queue的底层结构
在我们使用STL标准库里的stack、queue的时候,我们并没有传第二个模板参数。
使用自己实现的stack:
使用STL标准库中的stack:
那我们看一下STL标准库中stack、queue是怎么适配的:
可以看到,STL标准库中的stack和queue这里传了一个缺省的容器参数,我们使用的时候不传就会使用默认的,但是这个默认的它实际上既没有使用vector也没有使用list,而是使用了deque。
所以刚才我们模拟实现的stack和queue能不能用deque来适配呢?当然可以,因为STL标准库中就是这样做的。在这里我们先介绍这个新的容器:deque双端队列。
deque的简单介绍(了解)
deque的特性
deque重要的两个特性:
既支持随机访问也支持任意位置的插入删除。
也就是说它既有vector的优点(随机访问)又有list的优点(任意位置插入)。
deque的使用非常简单,这里不再实现了。
在这里,deque看似是一个“完美”的容器,融合了vector与list的优点,看起来好像是一个可以替代vector和list的容器。但是,deque真的是完美的吗?实际呢?
deque的缺陷
deque最大的缺陷就在于它的随机访问效率低。什么情况下能体现它的随机访问效率呢?sort排序(底层是快排,需要遍历随机访问,这里不做多解释)。
eg:随机生成一百万个数据排序用deque和vector分别来排序比较一下效率。
#include<iostream> #include<vector> #include<deque> #include<time.h> #include<algorithm> using namespace std; int main() { vector<int> v; deque<int> d; const int n = 1000000; srand(time(0)); for (int i = 0; i < n; i++) { int x = rand(); d.push_back(x); v.push_back(x); } size_t begin1 = clock(); sort(d.begin(), d.end()); size_t end1 = clock(); size_t begin2 = clock(); sort(v.begin(), v.end()); size_t end2 = clock(); cout << end1 - begin1 << endl; cout << end2 - begin2 << endl; return 0; }
可以看到效率相差了五倍左右。
也就因为这一个性能的差异就说明deque并没有那么完美,它也无法代替vector和list。
deque的原理介绍(了解)
deque(双端队列):虽然也叫队列,但是它不支持先进先出跟queue没关系。deque是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。



deque总结
与vector比较 :deque的优势是: 头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是比vector高的。与list比较:其 底层是连续空间,空间利用率比较高,不需要存储额外字段。但是,deque有一个致命缺陷: 不适合遍历, 因为在遍历时,d eque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下, 而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构
为什么选择deque作为stack和queue的底层默认容器
1. stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。2. 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。 结合了deque的优点,而完美的避开了其缺陷。
STL标准库中对于stack和queue的模拟实现
STL标准库中stack的模拟实现:
#include<deque> namespace zxa { template<class T, class Container = deque<T>> class stack { public: void push(const T& x) { _con.push_back(x); } void pop() { _con.pop_back(); } size_t size() { return _con.size(); } bool empty() { return _con.empty(); } T& top() { return _con.back(); } private: Container _con; }; }
STL标准库中queue的模拟实现:
#include<deque> namespace zxa { template<class T, class Container = deque<T>> class queue { public: void push(const T& x) { _con.push_back(x); } void pop() { _con.pop_front(); } size_t size() { return _con.size(); } bool empty() { return _con.empty(); } T& front() { return _con.front(); } T& back() { return _con.back(); } private: Container _con; }; }
四、priotity_queue的介绍和使用
priority_queue的介绍
priority_queue的文档介绍:
翻译:
1、 优先队列是一种 容器适配器 ,根据严格的 弱排序 标准,它的第一个元素总是它所包含的元素中最大的。2.、此上下文类似于 堆 ,在堆中可以随时插入元素,并且只能 检索最大堆元素 (优先队列中位于顶部的元素)。3.、优先队列被实现为容器适配器, 容器适配器 即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。4.、底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:
- empty():检测容器是否为空
- size():返回容器中有效元素个数
- front():返回容器中第一个元素的引用
- push_back():在容器尾部插入元素
- pop_back():删除容器尾部元素
5. 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类, 则使用vector。6. 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数 make_heap、push_heap和pop_heap来自动完成此操作。
priority_queue的使用
优先级队列 默认使用vector作为其底层存储数据的容器,在vector上又使用了 堆算法将vector中元素构造成堆的结构,因此priority_queue就是 堆,所有 需要用到堆的位置,都可以考虑使用priority_queue。注意: 默认情况下priority_queue是大堆。![]()
优先级高的先输出,priority_queue默认是一个大堆,大的优先级高,所以输出按从大到小的顺序排列。
模拟实现priority_queue
实现 push()接口:
通过对priority_queue的了解,我们已经知道priority_queue是一个堆,默认是大堆。
而我们每尾插一个元素,就要保持它的大堆结构。这里需要用到堆的向上调整来实现。
堆的向上调整(0(logN)):
实现 pop()接口:
要在优先级队列中删除堆顶元素,不能直接头删,因为底层是priority_queue的底层是通过vector来适配的,vector没有提供头删的操作,因为vector头删的效率极低。
这里需要先交换堆顶与最后一个元素的位置,再尾删,就删除了堆顶元素,之后再使用堆的向下调整算法维持大堆结构。
堆的向下调整(0(log(N)):
实现size()、top()、empty()接口:
测试:
说明:每次push一个元素都会向上调整,保持pq是一个大堆,最大的元素在堆顶,top取堆顶的数据,pop删除堆顶的数据之后,会向下调整,依旧保持pq是一个大堆,最大的元素依旧在堆顶。所以打印出结果是按优先级依次排列(这里是大堆,所以是大的优先级高)。正是由于push的向上调整和pop的向下调整很好的保持了priority_queue大堆的结构。
模拟实现priority_queue完整代码演示:
#include<vector> using namespace std; namespace zxa { template<class T, class Container = vector<T>> class priority_queue { public: void AdjustUp(int child) { int parent = (child - 1) / 2; while (child > 0) { if (_con[child] > _con[parent]) { swap(_con[child], _con[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } } void push(const T& x) { _con.push_back(x); AdjustUp(_con.size()-1); } void AdjustDown(int root) { int parent = root; int child = parent * 2 + 1; while (child < _con.size()) { if (child + 1 < _con.size() && _con[child + 1] > _con[child]) { ++child; } if (_con[child] > _con[parent]) { swap(_con[child], _con[parent]); parent = child; child = parent * 2 + 1; } else { break; } } } void pop() { swap(_con[0], _con[_con.size() - 1]); _con.pop_back(); AdjustDown(0); } T& top() { return _con.front(); } size_t size() { return _con.size(); } bool empty() { return _con.empty(); } private: Container _con; }; }
我们上述模拟实现了priority_queue,优先级高的先输出,默认是大堆,大的优先级高,所以输出按从大到小的顺序排列,但是如果我们想让小的优先级高、按从小到大的顺序排列呢?
非常简单,让priority_queue是一个小堆即可。
只需要调整AdjustUp和AdjustDown里的符号就可以让priority_queue维持一个小堆。
但是我们要写一份跟原来相似的代码是不是太冗余了,有没有一种方式可以很好的控制这些符号呢?这里我们就要引入仿函数。
仿函数
说明:这里的less和greater就是一个仿函数,也叫函数对象,本质就是一个重载了operator()的类。
为什么叫仿函数和函数对象呢?
因为它的对象可以像一个函数去使用,这里的 " lessFunc(1,2) " 和 " greaterFunc(1,2) " 看起来是一个函数,但是它不是,而是一个对象。这个对象去调用了它的operator(),本质是" lessFunc.operator()(1,2) " ," greaterFunc.operator () (1,2) "。
我们来看一下STL标准库里面的priority_queue。
可以看到这里的模板参数多传了一个缺省的Compare类。这个类是一个仿函数,默认是less<typename Container::value_type>弱排序。
注意:STL标准库中priority_queue默认是一个大堆,而这里传的却是一个less的仿函数也就是重载了小于的类。
了解了仿函数,我们就可以模拟实现STL标准库中的priority_queue了。
模拟实现STL标准库中的priority_queue
priority.h:#include<vector> using namespace std; namespace zxa { template<class T, class Container = vector<T>,class Compare = less<T>> class priority_queue { public: void AdjustUp(int child) { int parent = (child - 1) / 2; while (child > 0) { //if (_con[child] > _con[parent]) if (_com(_con[parent], _con[child])) { swap(_con[child], _con[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } } void push(const T& x) { _con.push_back(x); AdjustUp(_con.size() - 1); } void AdjustDown(int root) { int parent = root; int child = parent * 2 + 1; while (child < _con.size()) { //if (child + 1 < _con.size() && _con[child + 1] > _con[child]) if (child + 1 < _con.size() && _com(_con[child], _con[child+1])) { ++child; } //if (_con[child] > _con[parent]) if (_com(_con[parent], _con[child])) { swap(_con[child], _con[parent]); parent = child; child = parent * 2 + 1; } else { break; } } } void pop() { swap(_con[0], _con[_con.size() - 1]); _con.pop_back(); AdjustDown(0); } T& top() { return _con.front(); } size_t size() { return _con.size(); } bool empty() { return _con.empty(); } private: Container _con; Compare _com; }; }
测试:
#include<iostream> #include"priority_queue.h" using namespace std; namespace zxa { //仿函数 函数对象 template<class T> struct less { bool operator()(const T& x1, const T& x2) { return x1 < x2; } }; template<class T> struct greater { bool operator()(const T& x1, const T& x2) { return x1 > x2; } }; } int main() { zxa::priority_queue<int, vector<int>, less<int>> pq1; pq1.push(5); pq1.push(2); pq1.push(9); pq1.push(4); pq1.push(6); while (!pq1.empty()) { cout << pq1.top() << " "; pq1.pop(); } cout << endl; zxa::priority_queue<int, vector<int>, greater<int>> pq2; pq2.push(5); pq2.push(2); pq2.push(9); pq2.push(4); pq2.push(6); while (!pq2.empty()) { cout << pq2.top() << " "; pq2.pop(); } cout << endl; }
说明:
这里的pq1就模拟了STL库里的priority_queue,传的是重载了<的仿函数less,是一个大堆,大的优先级高,输出 “9 6 5 4 2”。
pq2传的是重载了>的仿函数greater,是一个小堆,小的优先级高,输出“2 4 5 6 9”。