C++初阶第十二篇—stack和queue(stack和queue的常见接口的用法与介绍+priority_queue+容器适配器+仿函数+模拟实现)
Posted 呆呆兽学编程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++初阶第十二篇—stack和queue(stack和queue的常见接口的用法与介绍+priority_queue+容器适配器+仿函数+模拟实现)相关的知识,希望对你有一定的参考价值。
⭐️今天我先为大家介绍STL中的stack和queue容器适配器,它的底层是用其其它容器来实现的,其后我会介绍另一个容器适配器——priority_queue(优先级队列)。
⭐️博客代码已上传至gitee:https://gitee.com/byte-binxin/cpp-class-code
目录
🌏stack
🌲stack的介绍
stack是一种先进后出的容器,之前的数据结构中有介绍过。
总结几点:
- stack是一种容器适配器,专门用在具有后进先出 (last-in first-out)操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
- stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
- stack的底层容器可以是任何标准容器,这些容器需要满足push_back,pop_back,back和empty几个接口的操作。
- 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque。
🌲stack的接口的介绍
- 构造函数 stack();
- empty 检测stack是否为空
- size 返回stack中元素个数
- top 返回栈顶元素
- push 从栈顶压入一个元素
- pop 从栈顶取出一个元素
实例演示:
void TestStack()
stack<int> s;
s.push(1);
s.push(2);
s.push(3);
s.push(4);
// 没有迭代器
while (!s.empty())
cout << s.top() << " ";
s.pop();
cout << endl;
代码运行结果如下:
🌏queue
🍯queue的介绍
queue是队列,是一种先进先出的容器。
总结几点:
- 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。
- 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
- 和stack一样,它的底层容器可以是任何标准容器,但这些容器必满足push_back,pop_back,back和empty几个接口的操作。
- 标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。
🍯queue的接口的介绍
- 构造函数 queue();
- empty 检测队列是否为空
- size 返回队列中元素个数
- front 返回队头元素的引用
- back 返回队尾元素的引用
- push 在队尾将一个元素入队
- pop 在队头将一个元素出队
实例演示:
void TestQueue()
queue<int> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
while (!q.empty())
cout << q.front() << " ";
q.pop();
cout << endl;
代码运行结果如下:
🌏容器适配器
适配器: 一种设计模式,该种模式是将一个类的接口转换成客户希望的另外一个接口。
stack和queue的底层结构
可以看出的是,这两个容器相比我们之间见过的容器多了一个模板参数,也就是容器类的模板参数,他们在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,它们的底层是其他容器,堆其他容器的接口进行了包装,它们的默认是使用deque(双端队列)(后面会介绍)
🌏deque的简单介绍
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。
deque底层结构
它并不是一段连续的空间,而是由多个连续的小空间拼接而成,相当于一个动态的二维数组。
如下图:
deque的迭代器:
deque的优点:
- 相比于vector,deque可以进行头插和头删,且时间复杂度是O(1),扩容是也不需要大量挪动数据,因此效率是比vector高的。
- 相比于list,deque底层是连续的空间,空间利用率高,,也支持随机访问,但没有vector那么高。
- 总的来说,deque是一种同时具有vector和list两个容器的优点的容器,有一种替代二者的作用,但不是完全替代。
deque的缺点:
- 不适合遍历,因为在遍历是,deque的迭代器要频繁地去检测是否运动到其某段小空间的边界,所以导致效率低下。
- deque的随机访问的效率是比vector低很多的,实际中,线性结构大多数先考虑vector和list。
下面是通过排序来测试vector和deque随机访问的效率
void TestDeque()
srand((unsigned int)time(nullptr));
deque<int> d;
vector<int> v;
for (size_t i = 0; i < 100000; ++i)
int randNum = rand();
v.push_back(randNum);
d.push_back(randNum);
int begin1 = clock();
sort(v.begin(), v.end());
int end1 = clock();
int begin2 = clock();
sort(d.begin(), d.end());
int end2 = clock();
cout << "vector排序用时:" << end1 - begin1 << "ms" << endl;
cout << "deque排序用时:" << end2 - begin2 << "ms" << endl;
代码运行结果如下:
容易看出,deque的随机访问的效率是比vector低很多的。
deque可以作为stack和queue底层默认容器的原因:
- stack和queue并不需要随机访问,也就是说没有触及到deque的缺点,只是对头和尾进行操作。
- 在stack增容时,deque的效率比vector高,queue增容时,deque效率不仅高,而且内存使用率也高。
🌏stack和queue的模拟实现
stl中的stack和vector是通过容器适配转换过来的,不是原生实现的,为了复用
- stack
template<class T, class Container = deque<T>>
class stack
public:
void push(const T& x)
_con.push_back(x);
void pop()
_con.pop_back();
T top()
return _con.back();
size_t size()
return _con.size();
bool empty()
return _con.empty();
private:
Container _con;
;
- queue
template<class T, class Container = deque<T>>
class queue
public:
void push(const T& x)
_con.push_back(x);
void pop()
_con.pop_front();
T& front()
return _con.front();
T& back()
return _con.back();
size_t size()
return _con.size();
bool empty()
return _con.empty();
private:
Container _con;
;
🌏priority_queue的介绍和使用
🐚priority_queue的介绍
总结几点:
- 优先级队列也是一种容器适配器,它的第一个元素总是最大的。
- 类似于堆,且默认是大堆,在堆中可以插入元素,并且只能检索最大元素。
- 底层容器可以任何标准容器类模板,也可以是其他特定容器类封装作为器底层容器类,需要支持push_back,pop_back,front和empty几个接口的操作。
🐚priority_queue的接口介绍
- 构造函数 priority_queue();
- empty 返回优先级队列是否为空
- size 返回优先级队列元素个数
- top 返回优先级队列中最大(最小)元素,即堆顶元素
- push 在优先级队列插入数据
- pop 删除堆顶元素
实例演示:
void test_priority_queue()
priority_queue<int, vector<int>> pq;
pq.push(5);
pq.push(7);
pq.push(4);
pq.push(2);
pq.push(6);
while (!pq.empty())
cout << pq.top() << " ";
pq.pop();
cout << endl;
代码运行结果如下:
🌏priority_queue的模拟实现
🌴priority_queue的小框架
其中模板中有三个参数,最后一个参数是仿函数(下面后介绍),也就是指明优先级队列是按照升序还是降序来存数据的。
template<class T, class Container = vector<T>, class Compare = less<T>>// 默认是小于
class priority_queue
public:
private:
Container _con;
Compare _com;
;
🌴仿函数
仿函数(functor),就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了。
实例如下:
// 仿函数 就是一个类重载了一个(),operator(),可以想函数一样使用
template<class T>
struct greater
bool operator()(const T& a, const T& b)
return a > b;
;
template<class T>
struct less
bool operator()(const T& a, const T& b)
return a < b;
;
可以看出,仿函数就是用一个类封装一个成员函数operator(),使得这个类的对象可以像函数一样去调用。
实例演示
template<class T>
struct IsEqual
bool operator()(const T& a, const T& b)
return a == b;
;
void test()
IsEqual<int> ie;
cout << ie(2, 3) << endl;// 该类实例化出的对象可以具有函数行为
代码运行结果如下:
🌴priority中的两个仿函数
// 仿函数 就是一个类重载了一个(),operator(),可以想函数一样使用
template<class T>
struct greater
bool operator()(const T& a, const T& b)
return a > b;
;
template<class T>
struct less
bool operator()(const T& a, const T& b)
return a < b;
;
🌴堆的向上调整和向下调整的实现
数据结构的博客有介绍过,这里直接上代码(都是在大堆或小堆的前提下操作):
向上调整: 从最后一个数往上调整
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 AdjustDown(int parent)
int child = parent * 2 + 1;
while (child < (int)size())
if (child + 1 < (int)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 AdjustUp(int child)
int parent = (child - 1) / 2;
while (child > 0)
if (_com(_con[parent], _con[child]))// _con[child] > _con[parent]
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
else
break;
void AdjustDown(int parent)
int child = parent * 2 + 1;
while (child < (int)size())
if (child + 1 < (int)size() && _com(_con[child], _con[child + 1]))// _con[child + 1] > _con[child]
++child;
if (_com(_con[parent], _con[child]))// _con[child] > _con[parent]
swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
else
break;
🌴priority_queue的插入和删除
- push 先在队尾插入数据,然后用向上调整算法使得堆是大堆或小堆。
void push(const T& x)
_con.push_back(x);
AdjustUp((int)size() - 1);
- pop 先将堆顶的元素和队尾的元素交换,再删去队尾元素(而不是直接删去堆顶元素,这样会破坏堆的结构,然后又要建堆),然后再使用向下调整算法使得堆是大堆或小堆。
void pop()
assert(!empty());
swap(_con[0], _con[(int)size() - 1]);
_con.pop_back();
AdjustDown(0);
🌴priority——queue的存取与大小
- top 返回堆顶元素
T& top()
assert(!empty());
return _con[0];
- size 返回优先级队列元素个数
size_t size()
return _con.size();
- empty 判断优先级队列是否为空
bool empty()
return size() == 0;
🌐总结
今天介绍的内容有点多,代码自己敲一敲,就会更加熟练。喜欢的话,欢迎点赞、收藏和关注支持~
以上是关于C++初阶第十二篇—stack和queue(stack和queue的常见接口的用法与介绍+priority_queue+容器适配器+仿函数+模拟实现)的主要内容,如果未能解决你的问题,请参考以下文章
C++初阶第十三篇—模板进阶(非类型模板参数+模板特化+模板的分离编译)
C++初阶第十篇——vector(vector常见接口的用法与介绍+vector的模拟实现)