🌈感谢阅读East-sunrise学习分享——stack & queue & 容器适配器 & prioity_queue & 反向迭代器
博主水平有限,如有差错,欢迎斧正🙏感谢有你
码字不易,若有收获,期待你的点赞关注💙我们一起进步
🌈今天分享一个设计模式 — 容器适配器,而通过这个设计模式也诞生了许多重要的容器以及… 话不多说,干货满满,我们即刻启程🚀
目录
- 一、stack的介绍和使用
- 1.stack的介绍和使用
- 2.经典题目
- 二、queue的介绍和使用
- 三、容器适配器
- 1.什么是适配器
- 2.适配器的使用
- 3.2deque缺陷
- 3.3选择deque作为stack和queue的底层默认容器
- 四、stack和queue的模拟实现
- 1.stack的模拟实现
- 2.queue的模拟实现
- 五、优先级队列
- 1.priority_queue的介绍
- 2.priority_queue的使用
- 3.仿函数
- 4.模拟实现
- 六、反向迭代器
- 1.反向迭代器的底层
- 2.反向迭代器的模拟实现
一、stack的介绍和使用
1.stack的介绍和使用
✏️stack的文档介绍
- stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
- stack是作为容器适配器被实现的(下文具体介绍),容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
stack的具体使用我们通过查看帮助文档便能清楚,我们之前的博客【数据结构】- 栈和队列也有过讲解,有了栈和队列的基础知识,对栈和队列的使用也更加的信手拈来了,这里博主就不对stack的使用进行过多赘述。
2.经典题目
📌最小栈
题目链接:155.最小栈
这道题的难点在于“在常数时间内检索到最小元素”🚩有的老铁就想着,定义一个变量咯,然后随时记录着,如果压栈的元素是目前栈中的最小元素,便更新
💥但是会有问题,我们的栈是也会pop的,当把目前你记录着的最小值给删了,那你又能怎么才能知道剩下的栈中元素的最小值是多少?
💥那不就得遍历,那遍历就无法符合它的时间复杂度要求了
所以以上的这个思路,只能记录一时,会有bug,那如何在有限的时间要求内又能实时地记录着最小元素 ---- 以空间换时间
🌏思路:
在创建题目要求的栈st的同时,我们再另外创建一个辅助栈minst
(用于实现getMin
)当我们在st.push( )
时比较后把最小元素也压入minst
中,在pop
时两个栈也按要求弹栈,使得minst
能随时和st保持一致,记录着栈中的最小元素
📈小优化,为了更节省空间,我们可以在对minst
压栈时判断一下,只在数据 <=
时才入栈,然后当st
和minst
相同时再一起出栈
✏️
class MinStack {
public:
MinStack() {
}
void push(int val) {
st.push(val);
if(minst.empty() || val <= minst.top())
minst.push(val);
}
void pop() {
//注意要先判断删除minst,再删除st,如果顺序颠倒会bug
if(st.top() == minst.top())
minst.pop();
st.pop();
}
int top() {
return st.top();
}
int getMin() {
return minst.top();
}
private:
//保存栈中的元素
std::stack<int> st;
//保存栈的最小元素
std::stack<int> minst;
};
二、queue的介绍和使用
✏️queue的文档介绍
- 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。
- 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
三、容器适配器
1.什么是适配器
在了解适配器之前,我们先了解:什么是设计模式
设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结
简单地说:通过长久的实践,总结出来的一种能够高效率、优效果解决需求目的的一种套路模式
比如我们之前学过的迭代器,在STL库中所有容器都有迭代器,因为迭代器好啊,迭代器也有被统一为:迭代器模式
迭代器模式:不暴露底层细节,封装后提供统一的方式访问容器
同理,适配器是一种设计模式,该模式是将已有的东西封装转换出你想要的东西🎈
2.适配器的使用
在上文对stack和queue的介绍中已经有提到,这两个容器不叫容器,准确来说是称为“容器适配器”正是因为他们是通过容器适配器这一设计模式实现的🎈
而到目前为止我们已经学过了不少容器,那么我们是否能自己实现呢?
拿queue为例子,在实现之前我们应该先考虑queue有什么需求
- 判空操作
- 返回队头元素的引用
- 返回队尾元素的引用
- 在队列尾部入队列
- 在队列头部出队列
看到这些操作,特别是头删,我们便想起上一篇博客对vector和list的优缺点的比较,我们选择list更为合适📌
namespace qdy
{
template<class T, class Container = list<T>>//模板参数使用缺省值,创建queue时便不用多传一个参数
class queue
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_front();
}
const T& front()
{
return _con.front();
}
const T& back()
{
return _con.back();
}
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
Container _con;
};
}
💭💭那stack呢?stack的对数据进行改动的需求好像就只是尾插尾删而已,而根据我们之前对list和vector的优缺点比较,stack底层的结构好像用vector更好一些,那这岂不是还得重新再换成vector的💤
这时我们就想着,要是能有一种容器,能兼具vector和list的优点就好了,当需要使用容器适配器时就用它🎈
你别说,诶你真别说,还真有🎯——deque
3.2deque缺陷
与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的
与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段
但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是STL用其做stack和queue的底层数据结构
3.3选择deque作为stack和queue的底层默认容器
stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器,比如vector和list都可以;queue是先进先出的特殊线性数据结构,只要具有push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如list。但是STL中对stack和queue默认选择deque作为其底层容器,主要是因为:
- stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
- 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。
四、stack和queue的模拟实现
1.stack的模拟实现
namespace qdy
{
template<class T,class Container = deque<T>>
class stack
{
public:
void push(cnost T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_back();
}
const T& top()
{
return _con.back();
}
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
Container _con;
};
}
2.queue的模拟实现
namespace qdy
{
template<class T, class Container = deque<T>>//模板参数使用缺省值,创建queue时便不用多传一个参数
class queue
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_front();
}
const T& front()
{
return _con.front();
}
const T& back()
{
return _con.back();
}
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
Container _con;
};
}
五、优先级队列
1.priority_queue的介绍
- 优先级队列也是一种容器适配器,默认情况下它适配的容器是vector。底层结构是堆,并且默认情况下是大堆
- 优先级队列,虽然名字中有“队列”但是不符合队列“先进先出”的规则;优先级队列的特点是:优先级高的元素先出(由于默认是大堆,所以默认情况下,数据大的优先级就高)
- 附上堆的博客介绍[数据结构] - 堆和priority_queue的文档介绍✏️优先级队列以便于更深刻了解
2.priority_queue的使用
优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意:默认情况下priority_queue是大堆。
函数声明 | 接口说明 |
---|---|
priority_queue( )/priority_queue(first, last) | 构造一个空的优先级队列 |
empty( ) | 检测优先级队列是否为空,是返回true,否则返回false |
top( ) | 返回优先级队列中优先级最高的元素(堆顶元素) |
push(x) | 在优先级队列中插入x |
pop( ) | 删除优先级队列中优先级最高的元素(堆顶元素) |
✏️多说无益,实践出真知
链接:215.数组中第k个最大元素
📌方法一:建一个大堆然后popk次(时间复杂度:O(N+K*logN))
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int> pq(nums.begin(),nums.end());
while(--k)
pq.pop();
return pq.top();
}
};
📌方法二:建一个大小为k的小堆,进行比较大小,若数据比堆的数据大,则替换后入堆
(时间复杂度:O(K+(N-K)*logK))
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
//建k个小堆
priority_queue<int,vector<int>,
greater<int>> pq(nums.begin(),nums.begin()+k);
//进行n-k次比较
for(int i = k; i < nums.size(); ++i)
{
if(nums[i] > pq.top())
{
pq.pop();
pq.push(nums[i]);
}
}
return pq.top();
}
};
💭💭看到方法二中,用priority_queue建小堆的写法不禁陷入了沉思…
上图是文档中优先级队列的介绍,前2个参数我们熟悉,也能知道底层默认是用vector来适配的,那第三个参数是什么?——仿函数
3.仿函数
仿函数的用处很多,今天我们通过优先级队列第一次接触到它,便来对其配合优先级队列进行简单地认识🎈
对于优先级队列来说,仿函数就像是一个灵活控制的开关,像上题的应用中,对于优先级队列我们有时候会想排小堆,有时候想排大堆;那系统封装了优先级队列的实现,总不能说直接去改代码逻辑吧?
所以priority_queue还提供了一个模板参数Compare,就是用于底层实现中创建堆的算法(系统默认调用less函数建大堆,所以如果我们是想建大堆则不需要传参)
🌏在之前使用C语言时,我们想要在函数调用时再传入函数进行算法调用,我们只能使用**“函数指针**”,但是函数指针的可读性实在是太差了,也无法支持泛型。于是在我们现在支持泛型模板时,便诞生了仿函数。
仿函数实际上就是一个函数对象,是一个通过类实例化出的对象,然后由于在类中重载了operator( ),在调用此对象是就像在函数调用一样,可以像函数一样去使用——因此叫做仿函数✨
namespace qdy
{
template<class T>
class less
{
public:
bool operator()(const T& a, const T& b)const
{
return a < b;
}
};
template<class T>
class greater
{
public:
bool operator()(const T& a, const T& b)const
{
return a > b;
}
};
}
我们写一个类再对函数调用的“()
”运算符进行重载,便能实现像函数调用一样的操作
int main()
{
qdy::less<int> lsFunc;
cout << lsFunc(1, 2) << endl;
//等价于
//cout << lsFunc.operator()(1,2) << endl;
qdy::greater<int> gtFunc;
cout << gtFunc(1, 2) << endl;
return 0;
}
4.模拟实现
🎈priority_queue底层结构是堆,建大堆or小堆时要进行比较,因此需要使用仿函数(系统默认建大堆——调用less——若想建小堆——调用)
namespace qdy
{
template<class T>
class less
{
public:
bool operator()(const T& a, const T& b)const
{
return a < b;
}
};
template<class T>
class greater
{
public:
bool operator()(const T& a, const T& b)const
{
return a > b;
}
};
template<class T,class Container = vector<T>,class Compare = less<T>>
class priority_queue
{
public:
priority_queue()
{}
//迭代器构造
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last)
:_con(first,last)
{
//建堆
for (int i = (_con.size() - 1 - 1) / 2; i >= 0; --i)
{
adjust_down(i);
}
}
void adjust_down(size_t parent)
{
Compare com;
size_t child = parent * 2 + 1;
while (child < _con.size())
{
//if(child + 1 < _con.size() && _con[child] < _con[child + 1] )
if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
++child;
//if(_con[parent] < _con[child)
if (com(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
void push(const T& x)
{
_con.push_back(x);
adjust_up(_con.size() - 1);
}
void adjust_up(size_t child)
{
Compare com;
size_t parent = (child - 1) / 2;
while (child > 0)
{
//if(_con[parent] < _con[child])
if (com(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
adjust_down(0);
}
const T& top() const
{
return _con[0];
}
bool empty() const
{
return _con.empty();
}
size_t size() const
{
return _con.size();
}
private:
Container _con;
};
}
🌏如果T是自定义类型
-
如果类中已支持比较大小,如:string类,我们可以直接使用
-
如果此自定义类型是我们自己写的类的话,我们需要对其“<” “>”操作符进行重载
-
假如原生的比较行为不是我们想要的,或是使用的是库里已经封装好的类但是不支持比较大小时,我们便需要写仿函数
六、反向迭代器
1.反向迭代器的底层
迭代器作为STL六大组件之一,其重要性在前文已经有涉及到;而迭代器除了const迭代器之外,还有一个重要的迭代器:反向迭代器
反向迭代器的实现同样也是使用了适配器模式🎈
在STL源码中对反向迭代器的实现,是通过正向迭代器进行构造
reverse_iterator rbegin(){return reverse_iterator(end());}
reverse_iterator rend(){return reverse_iterator(begin());}
我们知道,在正向迭代器中,end( )
是指向最后一个元素的下一个位置,而反向迭代器的实现为了追求对称,因此其rbegin( )同样如此
💭但是这样就会造成一个问题:当我们使用反向迭代器然后要解引用时,就会出现问题,因此在对反向迭代器的解引用运算符重载时要稍作修改
2.反向迭代器的模拟实现
namespace qdy
{
template<class Iterator,class Ref,class Ptr>
class Reverse_Iterator
{
public:
Reverse_Iterator(Iterator it)
:_it(it)
{}
Ref operator*()
{
Iterator tmp = _it;
return *(--tmp);
}
Ptr operator->()
{
return &(operator*());
}
Reverse_Iterator& operator++()
{
--_it;
return *this;
}
Reverse_Iterator& operator--()
{
++_it;
return *this;
}
bool operator!=(const Reverse_Iterator& s)const
{
return _it != s._it;
}
private:
Iterator _it;
};
}
🌈🌈写在最后
我们今天的学习分享之旅就到此结束了
🎈感谢能耐心地阅读到此
🎈码字不易,感谢三连
🎈关注博主,我们一起学习、一起进步