这篇文章主要目标:
stack、queue、priority_queue的介绍和使用
什么是容器适配器
-------
stack的介绍及使用
早在几个月前,我就写过一篇文章来讲解和模拟实现stack,当时用的是C语言的版本,这里放上链接:
【数据结构】栈_Knous的博客-CSDN博客_数据结构栈
因为现在使用的是C++,这里就重新说一下
stack的介绍
stack - C++ Reference (cplusplus.com)
基本可以参考这个网站,但因为是全英文,这里挑一下重点说:
1. stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
2. stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
3. stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
empty:判空操作
back:获取尾部元素操作
push_back:尾部插入元素操作
pop_back:尾部删除元素操作
4. 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque。
简单来说,stack是一种容器,这个容器可以用vector是实现,也可以用list实现,只要满足其基本操作需要,如果没有指定容器,就会用deque(双向循环队列)
其基本功能有尾插尾删,获得尾部数据及判空
stack的使用
这里说几个常用的函数
函数说明 | 接口说明 |
stack() | 构造空的栈 |
empty() | 检测stack是否为空 |
size() | 返回stack中元素的个数 |
top() | 返回栈顶元素的引用 |
push() | 将元素val压入stack中 |
pop() | 将stack中尾部的元素弹出 |
这里借一道题举例:
让我们实现一个栈,这个栈支持一些基本的操作,其中稍有难度的是最后有获取最小元素,要在常数时间里面,也就是说我们不能采用遍历的办法,这里说一个解法:
我们创建两个栈,第一个来完成插入删除,第二个来完成获取最小值的情况,假设st1(常规操作)要插入值,那就用st2(最小值)来判断
假如st2为空,或者插入的这个值比st2上一个值小,那我们最小值就要更新,这个值就要尾插到st2里面,获取最小值的时候就直接返回st2的顶部元素
这里看代码:
class MinStack {
public:
//两个栈,st1完成常规操作,st2来找最小
stack<int> st1, st2;
MinStack() {
}
void push(int val) {
//先插入st1
st1.push(val);
//如果走到这一步,st1里面肯定有起码一个值,此时如果st2为空,说明最小值就是那个元素,如果st2不为空,说明里面已有最小值,这个时候判断,如果这个插入值比当前最小值小,那就插入,最小值就要更新
if(st2.empty() || val <= st2.top())
{
st2.push(val);
}
}
//如果删的st1的值刚好是最小值,那st2也要删除,如果不是就直接删除st1即可
void pop() {
if(st1.top() == st2.top())
{
st1.pop();
st2.pop();
}
else
st1.pop();
}
//直接返回st1即可
int top() {
return st1.top();
}
//最小值都在st2里面,返回st2即可
int getMin() {
return st2.top();
}
};
模拟实现
template<class T, class con = deque<T>>
class stack
{
public:
private:
con _con;
};
一点一点看,首先这是一个模板,那就用一个T来接收类型,重点是后面的con,这个就是我们的适配器了,因为每次都要造轮子的话也太麻烦了,因为stack的基本操作可以用其他
vector、list实现,那为什么不直接拿来用呢?
所以就出现了这里的,con是一个类模板,这个默认的是用deque,如果不喜欢这个也可以改,不过暂时先用着
插入
stack的插入也只有尾插一种,所以这里直接调用con的函数:
void push(const T& x)
{
_con.push_back(x);
}
直接调用对应接口,由上面的deque来帮我们解决问题
删除
void pop()
{
_con.pop_back();
}
返回顶端元素
const T& top()
{
return _con.back();
}
这里说一下,vector和list都有这个back函数,它的作用是返回最后的数据,这里正好直接用
看一下效果;
不是默认用的deque吗,我传个vector行不行?
当然可以
list:
都没有问题
只要这里的类模板符合stack的需求,即尾部的插入删除,返回尾部元素等,都可以替换
------------
queue的介绍和使用
同样我写了一篇queue的c介绍和模拟实现,这里是链接:
同样,先放上较为官方的文档介绍,再说一下重点
queue - C++ Reference (cplusplus.com)
1. 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。
2. 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
3. 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
empty:检测队列是否为空
size:返回队列中有效元素的个数
front:返回队头元素的引用
back:返回队尾元素的引用
push_back:在队列尾部入队列
pop_front:在队列头部出队列
4. 标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque
这里要提一下:
queue不能使用vector来作为底层,因为queue要支持头删,而vector的头删效率太低,所以不建议使用
queue的使用
这里说几个基本函数:
函数声明 | 接口说明 |
queue() | 构造空的队列 |
empty() | 检测队列是否为空,是返回true,否则返回false |
size() | 返回队列中有效元素的个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素val入队列 |
pop() | 将队头元素出队列 |
模拟实现
因为大体实现思路是跟stack差不多的,所以这里就直接放上代码:
template<class T,class Con = deque<T>>
class queue
{
public:
void push(const T& x)
{
_con.push_back(x);
}
const T& front()
{
return _con.front();
}
const T& back()
{
return _con.back();
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
void pop()
{
return _con.pop_front();
}
private:
Con _con;
};
演示:
----------
提一个问题,要不要写构造函数?
上面的stack和queue都没有写,这是为什么?
因为其底层本质是借助容器适配器,在调用的时候容器适配器会完成构造,这里将没写
-------
priority_queue
这个名看着挺陌生的,但可以这么理解,这个所谓的优先级_队列,其实就是一个二叉堆,为什么这么说,可以看下面的部分。
---
priority_queue的介绍
priority_queue - C++ Reference (cplusplus.com)
翻译一下,就是;
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来自动完成此操作
其实看着一大堆,简化一下意思就是:
这是一个由vector或者deque完成的一个容器,里面有一块连续空间来存放数据,为什么不能用list,因为它要完成调整,平时第一个元素是这块连续空间最大的(或者最小的),所以每插入一个元素都要调整,这么看,像不像之前学的堆?
【数据结构】实现堆、堆的应用_Knous的博客-CSDN博客
堆我也写了介绍,这里有兴趣的可以去看看,我们模拟实现其实思路跟那个差不多
priority_queue的使用
函数声明 | 接口说明 |
priority_queue()/priority_queue(first, last) | 构造一个空的优先级队列 |
empty( ) | 检测优先级队列是否为空,是返回true,否则返回 false |
top( ) | 返回优先级队列中最大(最小元素),即堆顶元素 |
push(x) | 在优先级队列中插入元素x |
pop() | 删除优先级队列中最大(最小)元素,即堆顶元素 |
这里要注意的不多,只有一个点,默认建立的是大堆,如果是小堆,需要传一个仿函数进去,这里演示的时候多了一个头文件:
functional
这个其实算一个算法的头文件
接下来看一下效果
其实这就是堆排序了,每次都取最大的,依次取到最小的,所以是降序,如果要按照升序,那就要传一个greater过去:
这个所谓的仿函数是什么?
你看完模拟实现就知道了
priority_queue的模拟实现
template<class T,class Con = deque<T>>
class priority
{
private:
Con _con;
};
暂且只有这么多东西,一点一点来,首先写一个尾插看看
void push(const T& val)
{
_con.push_back(val);
}
这样写,对吗?
如果是这样写的话,那跟stack、queue有什么区别吗?
我们知道堆最大的特点就是它的最开头的元素是最大的(或者是最小的),为了实现这个特点,我们每插入一个数据的时候都要考虑它是否需要调整,这里就要写一个函数来完成,因为是从最后一个位置向上调,所以这个函数暂且就叫adjustup
插入
void push(const T& val)
{
_con.push_back(val);
AdjustUp(_con.size()-1);
}
void AdjustUp(int child)
{
size_t parent = (child - 1) / 2;
while (child > 0)
{
if (_con[parent] < _con[child])
{
swap(_con[parent], _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
return;
}
删除
所谓的删除,其实就是将开头位置的数据跟最后一个交换,然后再让size--,不过这里还有一个问题就是,最后一个元素其实是叶子节点,让叶子节点到了头节点上面,这就不符合堆的性质了,所以这里还要再写一个调整函数,因为是向下调整,所以这个就取名adjustdown
void pop()
{
if (_con.empty())
return;
else
{
swap(_con.front(), _con.back());
_con.pop();
AdjustDown(0);
}
}
void AdjustDown(int praent)
{
size_t child = praent * 2 + 1;
while (child < _con.size())
{
if (child + 1 < _con.size() && _con[child+1] > _con[child])
{
child += 1;
}
if (_con[praent] < _con[child])
{
swap(_con[praent], _con[child]);
praent = child;
child = praent * 2 + 1;
}
else
break;
}
return ;
}
容量、是否为空、顶端元素
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
const T& top()
{
return _con[0];
}
用迭代器区间来构造
template<class Inputiterator>
priority_queue(Inputiterator first, Inputiterator last)
{
while (first != last)
{
push(*first);
first++;
}
}
其实跟之前写的迭代器区间没什么区别,先插入,然后调整
测试:
没什么问题,但是又有点问题
就是,这个调整堆的部分是固定的,也就是说它只能单纯的是大堆或者是小堆,那么有没有办法可以解决这个问题呢?
当然有,这个比较函数不要定死就可以了,既然如此,就有了第三个模板参数
template<class T>
struct less
{
bool operatop(T& x,T& y) const
{
return x < y ? ;
}
};
template<class T,class Con = deque<T>,class cmp = less<T>>
借此,我们写了一个新的类出来
这个类名字就叫less,里面没有参数,只有一个运算符的重载,我们默认传过去的是less,到时候比较的时候我们就可以靠cmp来比较,然后判断交换
经过修改后的比较函数:
void AdjustUp(int child)
{
size_t parent = (child - 1) / 2;
while (child > 0)
{
if (cmp(_con[parent] , _con[child]))
{
swap(_con[parent], _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
return;
}
void AdjustDown(int praent)
{
size_t child = praent * 2 + 1;
while (child < _con.size())
{
if (child + 1 < _con.size() && cmp(_con[child], _con[child+1]))
{
child += 1;
}
if (cmp(_con[praent], _con[child]))
{
swap(_con[praent], _con[child]);
praent = child;
child = praent * 2 + 1;
}
else
break;
}
return ;
}
注意这里有一个坑,就是less其实就是一个类,作为一个类,它必须先创建一个对象出来,然后才能调用...
既然有了less,那就再写一个greater
template<class T>
struct greater
{
bool operator()(const T& x, const T& y) const
{
return x > y;
}
};
既然我们要调用这个greater,那就应该先传过去vector,如图:
那这个greater是什么?
其实这就是一个仿函数,这里就简单说一下,这里当然也可以用函数指针来代替,但却过于繁琐,不如我们直接重载一个括号比较,如果有兴趣可以看这篇博客
C++ 仿函数_恋喵大鲤鱼的博客-CSDN博客_c++仿函数
至此,stack和queue的模拟实现也已经完成,希望这篇文章对你的学习有所帮助