目录
1.容器适配器
2.stack
stack的常用接口及使用示例
stack的模拟实现
3.queue
queue的常用接口及使用示例
queue的模拟实现
4.priority_queue
priority_queue的常用接口及使用示例
priority_queue的模拟实现
5.deque
认识deque
deque底层的数据结构
deque和vector、list的对比
1.容器适配器
stack、queue和priority_queue在STL中并不属于容器,而是属于容器适配器,那什么是容器适配器呢?所谓容器适配器就是对其他容器的接口进行了封装,封装出自己想要的功能。
比如我们学习数据结构的时候:
- 当我们想要实现一个栈,由于栈只在一端进行进行元素的插入和删除,因此我们选择顺序表作为其底层存储数据的结构,对顺序表进行封装,从而实现栈。
- 当我们想要实现一个队列,由于队列需要进行头删和尾插,因此我们选择链表作为其底层存储数据的结构,对链表进行封装,从而实现队列。
举一个生活中的例子:比如我们的电脑没有typec接口,只有USB接口,可我们偏偏要使用typec接口,这个时候我们都会买一个适配器,适配出我们想要的接口以便达到我们想要的功能。
在STL中,对于stack和queue而言,其实就是默认对deque(双端队列)的接口进行封装。(详细过程后面讲解)
priority_queue默认对vector的接口进行封装
2.stack
stack其实就是栈这种数据结构,栈这种数据结构的特点是只能在一端进行数据的插入和删除,所以,栈的底层容器应该支持以下操作:
- push_back():尾插
- pop_back():尾删
- back():获取最后一个元素
- empty():判断是否为空
STL标准容器中的vector、list、deque均符合要求,但是默认情况下,STL采用deque作为其底层容器。
stack的常用接口及使用示例
常用接口:
- 判断是否为空:bool empty() const。
- 获取有效元素个数:size_type size() const。
- 取栈顶元素:
value_type& top();
const value_type& top() const; - 在栈顶插入数据:void push (const value_type& val)。
- 删除栈顶元素:void pop()。
使用示例:
#include <iostream>
#include <stack>using namespace std;void test_use_stack()
{stack<int> st; // 构造一个空的stackst.push(1); // 插入数据st.push(2);st.push(3);st.push(4);st.push(5);cout << "st.size(): " << st.size() << endl;while (!st.empty()) // 当st不为空时{auto e = st.top(); // 取栈顶元素cout << e << ' ';st.pop(); // 删除栈顶元素}
}int main()
{test_use_stack();return 0;
}运行结果:
st.size() : 5
5 4 3 2 1
stack的模拟实现
我们默认采用vector为stack的底层容器,对vector的接口进行封装即可实现特定功能的stack。
#include <vector>namespace xy // 防止命名冲突
{/** T:元素的数据类型* Container:底层容器,我们默认其为vector*/template<class T, class Container = std::vector<T>>class stack{public:void push(const T& x) // 栈顶插入元素,复用vector的push_back实现{_con.push_back(x);}void pop() // 删除栈顶元素,复用vector的pop_back实现{_con.pop_back();}const T& top() // 取栈顶元素,复用vector的back实现{return _con.back();}size_t size() // 获取元素个数,复用vector的size实现{return _con.size();}bool empty() // 判断是否为空,复用vector的empty实现{return _con.empty();}private:Container _con; // 对指定容器进行封装};
}
3.queue
queue其实就是队列这种数据结构,队列这种数据结构的特点是元素在队尾入队列,在队头出队列,所以,queue的底层容器应该支持以下操作:
- empty:判断是否为空
- size:返回队列中有效元素的个数
- front:返回队头元素的引用
- back:返回队尾元素的引用
- push_back:在队列尾部插入数据
- pop_front:在队列头部删除数据
STL标准容器中的list、deque均符合要求,但是默认情况下,STL采用deque作为其底层容器。
queue的常用接口及使用示例
常用接口:
- empty():检测队列是否为空,是返回true,否则返回false。
- size():返回队列中有效元素的个数。
- front():返回队头元素的引用。
- back():返回队尾元素的引用。
- push():在队尾将元素val入队列。
- pop():将队头元素出队列。
使用示例:
#include <iostream>
#include <queue>using namespace std;void test_use_queue()
{queue<int> q; // 构造空的队列q.push(1); // 入队列q.push(2);q.push(3);q.push(4);q.push(5);// 获取有效元素的个数cout << "q.size(): " << q.size() << endl;// 获取队头元素cout << "q.front(): " << q.front() << endl; // 获取队尾元素cout << "q.back(): " << q.back() << endl;while (!q.empty()) // 判断队列是否为空{cout << q.front() << ' '; q.pop(); // 出队列}
}int main()
{test_use_queue();return 0;
}运行结果:
q.size() : 5
q.front() : 1
q.back() : 5
1 2 3 4 5
queue的模拟实现
我们实现的queue底层采用std中的list,对list的接口进行封装即可得到特定功能的queue。
#include <list>namespace xy
{/** T:存储的数据类型* Container:queue的底层容器,我们采用std中的list*/template<class T, class Container = std::list<T>>class queue{public:void push(const T& x) // 入队列,复用list的push_back实现{_con.push_back(x);}void pop() // 出队列,复用list的pop_front实现{_con.pop_front();}const T& front() // 获取队头元素,复用list的front实现{return _con.front();}const T& back() // 获取队尾元素,复用list的back实现{return _con.back();}size_t size() // 获取有效元素的个数,复用list的size实现{return _con.size();}bool empty() // 判断queue是否为空,复用list的empty实现{return _con.empty();}private:Container _con; // queue的底层容器};
}
4.priority_queue
priority_queue(优先级队列)其实就是堆这种数据结构,堆这种数据结构的特点是元素在容器的尾部插入和弹出,所以,priority_queue的底层容器应该支持以下操作:
- empty():检测容器是否为空。
- size():返回容器中有效元素个数
- front():返回容器中第一个元素的引用
- push_back():在容器尾部插入元素
- pop_back():删除容器尾部元素
STL标准容器中的vector和deque均符合要求,但是默认情况下,STL采用vector作为其底层容器。
注意:虽然list也满足这些接口的要求,但是不能将list作为priority_queue的底层数据结构。因为priority_queue是符合堆的特点的,堆的底层其实是连续存储的物理空间,堆的相关操作涉及下标的随机访问,list并不具备这一点,所以不能将list作为priority_queue的底层数据结构。
priority_queue的常用接口及使用示例
优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,priority_queue的使用就是堆的使用。
注意:默认情况下priority_queue是大堆。
如果读者不清楚堆相关知识,推荐阅读:
数据结构 —— 堆https://blog.csdn.net/D5486789_/article/details/142999937?spm=1001.2014.3001.5501常用接口:
- empty:检测优先级队列是否为空,是返回true,否则返回false。
- size:获取有效元素的个数。
- top:获取堆顶元素
- push:在优先级队列中插入元素。
- pop:删除堆顶元素。
使用示例:
#include <iostream>
#include <vector>using namespace std;void test_use_priority_queue()
{// 默认建立大堆// priority_queue<int> heap;// 构建小堆:指定底层容器为vector,比较方式为greater,建立小堆priority_queue<int,vector<int>,greater<int>> heap;heap.push(1); // 向堆中插入数据heap.push(5);heap.push(3);heap.push(2);heap.push(4);// 获取有效元素的个数cout << "heap.size(): " << heap.size() << endl; while (!heap.empty()) // 判断是否为空{cout << heap.top() << ' '; // 获取堆顶元素heap.pop(); // 删除堆顶元素}
}int main()
{test_use_priority_queue();return 0;
}运行结果:
heap.size() : 5
1 2 3 4 5
priority_queue的模拟实现
我们实现priority_queue的时候将vector作为底层容器,对vector的接口进行封装,并配合堆的向上和向下调整算法,让底层容器中的元素保持堆的特点,从而实现priority_queue。
#include <iostream>
#include <vector>using namespace std;namespace xy
{// 当堆是使用该比较方式的时候,建立大堆template<class T> struct less{bool operator()(const T& left, const T& right){return left < right;}};// 当堆是使用该比较方式的时候,建立小堆template<class T>struct greater{bool operator()(const T& left, const T& right){return left > right;}};/** T:存储的数据类型* Container:priority_queue的底层容器,默认使用std中的vector* Compare:堆中元素的比较方式,控制建立大堆还是小堆,默认建立大堆*/template<class T, class Container = std::vector<T>, class Compare = less<T>>class priority_queue{private:Container _con;private:// 向上调整void AdjustUP(int child){int parent = ((child - 1) >> 1);while (child){if (Compare()(_con[parent], _con[child])){swap(_con[child], _con[parent]);child = parent;parent = ((child - 1) >> 1);}else{return;}}}// 向下调整void AdjustDown(int parent){size_t child = parent * 2 + 1;while (child < _con.size()){// 找以parent为根的较大的孩子if (child + 1 < c.size() && Compare()(_con[child], _con[child + 1]))child += 1;// 检测双亲是否满足情况if (Compare()(_con[parent], _con[child])){swap(_con[child], _con[parent]);parent = child;child = parent * 2 + 1;}elsereturn;}}public:// 创造空的优先级队列priority_queue():_con(){}// 向堆中插入元素,复用尾插之后,进行向上调整保持堆的特点。void push(const T& data){_con.push_back(data);AdjustUP(_con.size() - 1);}// 删除堆顶元素void pop(){if (empty())return;swap(_con.front(), _con.back()); // 替换法删除_con.pop_back();AdjustDown(0); // 删除后进行向下调整保持堆的结构}size_t size()const{return _con.size();}bool empty()const{return _con.empty();}// 堆顶元素不允许修改,因为:堆顶元素修改可以会破坏堆的特性const T& top()const{return _con.front();}};
}
注意:对于自定义类型,我们需要自己在自定义类型中提供 > 和 < 运算符重载。
5.deque
认识deque
经过前面的学习,我们已经知道了stack和queue都采用deque作为底层容器,那deque到底是个什么东西呢?下面我们来看看deque这种数据结构。
deque叫做双端队列,是一种双开口的,具有 "连续" 空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1)。
- deque与vector比较,头插效率高,不需要搬移元素。
- deque与list比较,空间利用率比较高。
说明一下:deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组。
deque底层的数据结构
其底层结构如下图所示:每一段连续的小空间由一个指针数组map管理,当存储空间不够时,只需要开辟一段空间之后,将其添加到map中即可,当map不够时,需要扩大map的空空间。
为了维护其 "整体连续" 以及随机访问的假象,这个艰巨的任务就落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂了。
deque迭代器的设计如下:迭代器有四个成员。
- first:指向一个buffer的开始。
- last:指向一个buffer的结束。
- cur:指向一个buffer中的一个元素。
- node:指向存储当前buffer指针的中控位置。
deque借助迭代器维护其假象的连续结构如下图所示:
- start迭代器标识第一个buffer,其中的cur成员指向这个buffer中的第一个数据。
- finish迭代器标识最后一个buffer,其中的cur成员指向这个buffer中的最后一个数据的下一个位置。
deque和vector、list的对比
vector、list、deque的优缺点:
优点 | 缺点 | |
vector | 1.支持下标的随机访问 | 1.前面和中间进行插入删除效率低 |
2.CPU缓存命中率高(物理空间连续的优势) | 2.扩容有消耗 | |
list | 1.任意位置插入删除效率高 | 1.不支持下标的随机访问 |
2.空间按需申请和释放,无扩容消耗 | 2.CPU缓存命中率低 | |
deque | 头插头删,尾插尾删效率高 | 中间插入删除效率低 |
下标随机访问效率不错,但和vector相比还有一定差距。 |
deque的缺陷:
- 与vector比较,deque的优势是:前面和中间位置插入、删除元素时,不需要搬移元素,效率特别高;而且在扩容时,也不需要搬移大量的元素,因此,其效率是比vector高的。
- 与list比较,其底层是连续空间,支持随机访问,并且空间利用率比较高,因为不需要存储额外字段。
但是,deque有一个致命缺陷 —— 不适合遍历。因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式容器的应用场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。
- stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器,比如vector和list都可以。
- queue是先进先出的特殊线性数据结构,只要具有push_back()和pop_front()操作的线性结构,都可以作为queue的底层容器,比如list。
那为什么选择deque作为stack和queue的底层容器呢?
- stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
- 在stack中元素增长时,deque比vector的效率高,因为扩容时不需要挪动大量数据。
- queue中的元素增长时,deque不仅效率更高,而且内存使用率也更高。
选择deque作为stack和queue的底层容器,其结合了deque的优点,而完美的避开了deque的缺点。