C++STL之stack,queue与容器适配器 - 教程
目录
- C++STL之栈与队列,容器适配器
- 容器适配器介绍
- 栈(Stack)与队列(Queue)
- 一、 栈(Stack):后进先出
- 二、 队列(Queue):先进先出
- 三、 优先级队列(Priority Queue):优先级最高的元素先出
- 四、双端队列(Deque):队头队尾可进可出
C++STL之栈与队列,容器适配器
容器适配器介绍
什么是容器适配器?
适配器是一种设计模式,它就像一个转换头,能将一个类的接口转换成用户所希望的另一种接口。STL中的stack、queue和priority_queue都是容器适配器,它们没有自己独立的数据结构,而是“适配”了其他容器(如deque, vector)的接口,提供了新的、特定的行为。
优点:
容器适配器通过封装和限制接口,将通用的顺序容器转化为具有特定语义的专用数据结构。
首先在代码清晰度方面,使用 stack 比直接用 vector 并约定只能操作尾部更能明确表达程序员的意图;并且容器适配器防止了误操作,因为开发者无法随意访问(不提供相应接口,通过封装后只暴露需要的功能接口)栈中间或队列中间的元素,强制遵守特定数据结构的访问规则;同时这种设计还具有很好的复用性,无需重新实现栈、队列等基础数据结构,而是直接复用现有容器的功能,给用户带来更加便捷的编程方式。
如何实现
我们在自定义类的时候可以定义为模板类,这就需要我们在实例化的时候传递相应的类型,同理,我们可以传入容器的类型,应该类型去定义成员属性,同时对该成员属性的方法进行封装,实现我们所需要的方法。
如下所示:
template <class T, class container>class stack{public://…… 封装相关方法private:container _st; //该容器被适配成stack}int main(){stack<int, vector<int>> vst; //底层使用vecotr,调用vecotr方法实现的栈stack<int, list<int>> lst; //底层使用list,调用list方法实现的栈return 0;}
栈(Stack)与队列(Queue)
一、 栈(Stack):后进先出
栈是一种严格遵守后进先出(LIFO) 原则的线性数据结构。你可以把它想象成一个只有一个口的圆筒,我们只能从这个口放入或取出物品,最后放进去的,总是最先被拿出来。
栈的实现与使用:
栈在C++标准库中并未作为一个独立的容器实现,而是作为一种**“容器适配器”**。这意味着它是在其他基础容器(如vector、deque、list)之上,通过封装其特定接口(只提供push_back, pop_back, top等操作)而构建的。默认情况下,stack使用deque作为其底层容器。
它的接口非常直观:
- push(val):将元素val压入栈顶。
- pop():从栈顶弹出一个元素。
- top():获取栈顶元素的引用。
- empty()和size():分别用于判断栈是否为空和获取元素个数。
- swap(st):与栈st进行交换
无迭代器的原因:
栈只能后进先出,因此没有遍历或者访问处于中间位置的元素的诉求,因此没有迭代器
模拟实现:
由于其行为与只在一端操作的线性表(vector,list,dqueue)高度相似,我们可以轻松地用已经存在的线性表来适配出一个栈,将所有插入和删除操作都限制在线性表的尾部即可。
构造函数和析构函数与可以使用编译器默认生成的,因为只有一个成员变量(Container _st),而且是自定义类型,会去调用他自己的构造函数和析构函数,
#pragma once
#include<iostream>#include<vector>#include<list>namespace mystack{template<class T, class Container = std::vector<T>> //默认使用vecotr<T>, 然而在C++STL标准中默认使用的是dqueueclass stack{private:Container _st;public:void push(const T& val){_st.push_back(val);}void pop(){_st.pop_back();}const T& top(){return _st.back();}bool empty(){return _st.empty();}size_t size(){return _st.size();}void swap(stack& val){_st.swap(val._st);}};}int main(){mystack::stack<int> st1; //默认使用vecotr<int> 作为底层容器for (int i = 0; i < 5; i++){st1.push(i);}mystack::stack<int> st2;for (int i = 5; i < 9; i++){st2.push(i);}st1.swap(st2);while (!st1.empty()){std::cout << st1.top() << ' ';st1.pop();}std::cout << std::endl;while (!st2.empty()){std::cout << st2.top() << ' ';st2.pop();}mystack::stack<int, std::list<int>> st3; //也可以使用list作为底层容器 mystack::stack<int, std::dqueue<int>> st4; //dqueue作为底层容器 //……return 0;}
二、 队列(Queue):先进先出
队列则遵循先进先出(FIFO) 的原则,就像现实生活中的排队。最早进入队伍的人,将最早被服务。
队列的实现与使用:
和栈一样,queue也是一种容器适配器。它要求底层容器必须支持从后端插入(push_back)和从前端删除(pop_front)。vector,list和deque都满足这个要求,默认情况下,它使用deque作为底层容器。
其核心接口包括:
- push(val):元素val进入队尾。
- pop():队头元素出队。
- front()和back():分别获取队头和队尾元素的引用。
- empty()和size():分别用于判断队列是否为空和获取元素个数。
- swap(que):与que队列进行交换
无迭代器的原因:
与栈相同,队列也没有遍历或者访问处于中间位置的元素的诉求,因此没有迭代器
模拟实现:
与栈的模拟实现相同,选择一个使用适配器模式可以很轻松的实现一个队列
由于queue需要在两端操作,而vector的头删效率很低,因此在模拟实现时,通常选择list作为底层容器,可以高效地进行头删和尾插。
#pragma once
#include<iostream>#include<vector>#include<list>namespace myqueue{template<class T, class Container = std::list<T>>class queue{private:Container _que;public:void push(const T& val){_que.push_back(val);}void pop(){_que.pop_front();}const T& back(){return _que.back();}const T& front(){return _que.front();}bool empty(){return _que.empty();}size_t size(){return _que.size();}void swap(queue& val){_que.swap(val._que);}};}int main(){myqueue::queue<int> que1; //默认list<int> 为底层容器myqueue::queue<int> que2;for (int i = 0; i < 5; i++){que1.push(i);}for (int i = 5; i < 9; i++){que2.push(i);}//que1.swap(que2);while (!que1.empty()){std::cout << que1.front() << ' ';que1.pop();}std::cout << std::endl;while (!que2.empty()){std::cout << que2.front() << ' ';que2.pop();}std::cout << std::endl;myqueue::queue<int, std::vector<int>> que3; //也可以使用vector作为底层容器,但是效率低,极其不推荐 myqueue::queue<int, std::dqueue<int>> que3; //dqueue作为底层容器 //……return 0;}
三、 优先级队列(Priority Queue):优先级最高的元素先出
优先级队列不再是简单的先进先出或后进先出,它其中的元素都带有“优先级”。出队时,永远是当前队列中优先级最高(或最低)的那个元素先出去。它的底层本质就是一个堆。
优先级队列的实现与使用:
priority_queue也是一个容器适配器,默认使用vector作为底层容器,并在其上应用堆算法(如make_heap, push_heap, pop_heap)来维护堆结构。默认情况下,它是一个大堆,即堆顶元素最大。
我们可以通过模板参数来控制它是大堆还是小堆:
- priority_queue< int >:默认,大堆。
- priority_queue<int, vector< int >, greater< int >>:小堆。
其核心接口包括:
- push(val):元素val插入队列,其插入位置由内部的堆维护。
- pop():删除堆顶元素(最大的或者最小的)。
- top():获取堆顶元素
- empty()和size():分别用于判断队列是否为空和获取元素个数。
- swap(pque):与pque优先级队列进行交换
自定义类型:
如果我们要在优先级队列中存放自定义类型(比如一个 less 类),就必须在该类中重载 <
或 >
运算符,因为优先级队列需要知道如何比较这些对象的优先级。
我们可以重载( )来作为比较的运算符,完全模拟库中的less和greater
template <class T>struct less{bool operator()(const T& x, const T& y){return x < y;}};template <class T>struct greater{bool operator()(const T& x, const T& y){return x > y;}};int main(){less<int> cmp; //生成该类的对象cmp(1, 2); //运算符重载,返回 1 < 2 的值cmp(3, 1);return 0;}
有了比较的方法,我们就可以控制最大堆和最小堆。
在实现的时候,核心在与如何维护任一节点都大于其后代节点,我们可以通过向上调整和向下调整来维护
当插入一个值的时候,我们将其放在最后一个叶子节点,然后通过向上调整来使堆合法化
当删除堆顶元素的时候,我们将堆顶元素与最后一个叶子节点交换,然后向下调整来使堆合法化
模拟实现代码:
#pragma once
#include<iostream>#include<vector>#include<list>namespace mypriority_queue{template <class T>struct less{bool operator()(const T& x, const T& y){return x < y;}};template <class T>struct greater{bool operator()(const T& x, const T& y){return x > y;}};template<class T, class Container = std::vector<T>, class Compare = less<T> >class priority_queue{private:Container _pque;public:// 0//1 2//3 4 5 6void AdjustUp(){int child = _pque.size() - 1;int parent = (child - 1) / 2;Compare com;while(child > 0){if(com(_pque[parent], _pque[child])){std::swap(_pque[parent], _pque[child]);}else{break;}child = parent;parent = (child - 1) /2;}}void AdjustDown(){int parent = 0;int child = parent * 2 + 1;Compare com;while(child < _pque.size()){if(child + 1 < _pque.size() && com(_pque[child] , _pque[child + 1])){child++;}if(com(_pque[parent], _pque[child])){std::swap(_pque[parent], _pque[child]);}else{break;}parent = child;child = parent * 2 + 1;}}void push(const T& val){_pque.push_back(val);AdjustUp();}void pop(){std::swap(_pque[0], _pque[_pque.size()-1]);_pque.pop_back();AdjustDown();}const T& top(){return _pque.front();}bool empty(){return _pque.empty();}size_t size(){return _pque.size();}void swap(priority_queue& val){_pque.swap(val._pque);}};}int main(){mypriority_queue::priority_queue<int, std::vector<int>, mypriority_queue::greater<int>> qu ;qu.push(4);qu.push(9);qu.push(7);qu.push(2);qu.push(5);qu.push(4);qu.push(9);qu.push(7);while (!qu.empty()){std::cout << qu.top() << ' ';qu.pop();}return 0;}
四、双端队列(Deque):队头队尾可进可出
deque(双端队列)是一个两端都能高效插入删除的数据结构,但它并非真正连续,而是由多个分段连续的空间组成。他结合了vector和list的优点,中和他两个的效率,但同时也带来了弊端
- 优点:头尾插删效率都是O(1),且扩容时代价比 vector 小。
- 缺点:遍历效率较低,因为迭代器需要频繁检查是否跨越了段边界。
具体实现:
deque的底层存在一个中控数组(指针数组),其内部存放的是指向一个个内存块的指针,每个内存块中可以存放一定量的数据。
这个中控数组从中间开始存放数据,也就是说,中控数组的头部一开始是空的,这样再进行头插的时候就无需挪动数据,如果中控数组中第一个有效的数据块存放满了,就重新开辟一块空间,然后在这块空间的尾部存放数据,如果没有满,那就在第一块有效数据块从后往前找没有存放数据的空间存放数据。
若是中控数组也满了,那么进行扩容,只需扩大中控数组,复制其内部的指针,无需复制内存块内容,因此其扩容效率大大提升
deque的接口与vector几乎相同,这里不过多介绍。
STL选择 deque 作为 stack 和 queue 的默认底层容器,是一个精妙的权衡:
- 无需遍历:stack 和 queue 不支持遍历,完美避开了 deque 遍历效率低的缺陷。
- 高效扩容:对于stack,deque 在元素增长时比 vector 效率更高(无需大量搬移数据)。对于queue,deque 同时具备了高效率和较高的内存利用率。
总结来说,deque 在 stack 和 queue 的应用场景中,恰到好处地结合了 vector 和 list 的优点,同时规避了自身的缺陷
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/937015.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!