本期我们来学习stack和queue
目录
stack介绍
栈的使用
栈的模拟实现
queue介绍
队列的使用
队列的模拟实现
deque
优先级队列
模拟实现
仿函数
全部代码
1. stack 是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。2. stack 是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部( 即栈顶 ) 被压入和弹出。3. stack 的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:empty :判空操作back :获取尾部元素操作push_back :尾部插入元素操作pop_back :尾部删除元素操作4. 标准容器 vector 、 deque 、 list 均符合这些需求,默认情况下,如果没有为 stack 指定特定的底层容器,默认情况下使用deque 。
我们发现,stack模板参数和我们之前的容器是不同的,我们现在只要知道,我们的栈和队列,是容器适配器,是用容器转换出来的,适配器的本质是一种复用
这些接口根据我们目前的水平是可以轻松看懂的
并且我们发现,它是没有迭代器的,不支持我们随便进行遍历的,因为要保持后进先出,队列也是一样的
使用起来是非常简单的
我们前面说过适配,我们来完成一个链式栈和数组栈秒切换
namespace bai
{
template
class stack
{
public:
void push(const T& x)
{
}
private:
Container _con;
};
void test() {
stack> st1;
stack> st2;
}
}
我们加一个模板参数就可以了
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();
}
这些接口我们全部复用即可,这就是适配器,非常方便
其中top我们使用back接口
list和vector都有back和front接口,我们直接使用
容器适配器就是,数据是容器管理,我们对容器进行封装,管理,改造等等
测试一下,也没有问题
不过此时我们还有一个问题,我们每次都要传两个模板参数,非常麻烦,库里面是不需要的
它给的是类型,给了一个deque,deque我们后面解释
我们先加一个缺省参数,给一个vector即可
此时不写默认就是vector的栈
另外,我们的栈是不需要构造函数,拷贝构造这些,因为我们是自定义类型,回去调用自己的构造,拷贝构造等等
1. 队列是一种容器适配器,专门用于在 FIFO 上下文 ( 先进先出 ) 中操作,其中从容器一端插入元素,另一端提取元素。2. 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类, queue 提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。3. 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:empty :检测队列是否为空size :返回队列中有效元素的个数front :返回队头元素的引用back :返回队尾元素的引用push_back :在队列尾部入队列pop_front :在队列头部出队列4. 标准容器类 deque 和 list 满足了这些要求。默认情况下,如果没有为 queue 实例化指定容器类,则使用标准容器deque 。
同样的,这些接口都很简单,这里就不再演示
队列用vector适配是不好的,但我们也可以强制适配
vector是没有提供头删的
不过我们可以使用erase来强制适配
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
//_con.pop_front();
_con.erase(_con.begin());
}
T& front()
{
return _con.front();
}
T& back()
{
return _con.back();
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
修改一下top,为front,再加一个back接口即可
测试一下也没有问题
但是这样强制适配是不好的
库里面是支持list,但不支持vector的
因为顺序表的头删效率是很低的
所以queue的默认容器我们使用list
deque是一个双端队列,不过它虽然看起来是队列,但其实不是
队列的特点是先进先出,但deque不是,它是一个双向开口的,两边都可以插入删除,而且他是一个随机迭代器
它就像一个vector和list合集的六边形战士,可以像数组一样访问,还有头插头删,尾插尾删
我们简单用一下
那deque这么强,为什么我们之前没怎么听说过呢?为什么不用它代替vector和list呢?
其实只看头插头删,尾插尾删这些,deque还是可以的,但论综合性,是无法代替vector和list的
这是100w数据量下,第一个deque是将数据拷贝到vector里,然后进行排序,再拷贝回来,第二个是直接对deque进行排序,差距是很明显的
deque其实是一个很一般的设计,这也想要,那也想要,最后啥也干不了
vector直接下标随机访问,但是扩容,中间以为头部的插入删除效率不高,list则是支持任意位置插入删除,但不支持下标随机访问,是按需要求申请释放的
vector是连续的,list的一个一个的节点,所以就有人想,我们开辟一段一段的空间,然后用一个中控数组指向他们(这个中控数组不是从头开始指向的), 中控数组就是一个指针数组,这样一看感觉和vector
指针数组满了,要进行扩容,不过扩容的代价是非常底的,它指向的空间是不用动的,只需拷贝指针,不需要拷贝原数据,我们上面说,中控数组是从中间开始指向的,所以头插时,是可以从前面的空间插入的
就像这样,而且它的插入是倒着往前走的 ,先插入的-1,再插入的-2
相比vector,它缓解了扩容的问题,头插头删问题,但是 [ ] 不够极致,[ ] 要去计算在哪个buff(指向的数组)的第几个
它要先计算在不在第一个buff,第一个buff不是满的,不是从零开始的,第一个buff是倒着往前走的,如果在的话就找位置访问,如果不在,i -=第一个buff数组的size,第几个buff = i / buff,在这个buff的第几个 = i % buffsize
所以是不够极致的,vector是直接指针解引用访问就行了,而这里是需要计算的,这也是为什么上面效率比对时差距明显的原因了,在大量访问时,这个计算是很麻烦的
相比list,它支持下标随机访问,cpu高速缓存效率不错,头尾插入删除不错,但是中间的插入删除就不好了,中间插入的话一个是挪动数据,这个效率太低,另一个就是扩容,扩容中间这个buff,是不好的,库里面也没有这么做,这样做会影响上面的计算,会使[ ]的效率进一步下降
只有高频的头尾的插入删除,才适合deque,所以deque就用来适配栈和队列的默认容器
deque的底层是非常复杂的
我们再看一张图
start就相当于begin,finish就相当于end,要进行一遍迭代器遍历,把begin给it,然后解引用,解引用返回*cur
这是源码 ,我们再看看++,有两个操作,first和last是指向这段数组的起始和末尾,cur!=last 说明没有走完,++cur即可,如果走完了,迭代器要指向下一个buff,里面的node就要++,指向下一个buff的地址,再解引用就可以指向下一个buff,然后再让first和last指向这个buff,cur再指向这个buff,我们再看什么适合结束,当it和end在同一个buff时,cur还没走完,当cur等于end的cur时就结束
deque我们稍微看看就行
优先级队列和deque一样,都不能算队列,因为队列是先进先出的,这两个只是占了个名
我们看到它也是容器适配器,默认使用了vector
这些接口也都很简单
优先级队列底层就是二叉树的堆,默认大的优先级高
默认是大堆,我们仔细看默认容器适配器传的是小于(第三个模板参数),但默认又是大堆 ,是反过来的
传一个greater才能变为小堆 ,而默认的less是大堆,是相反的,大家要牢记
不一样的地方是构造函数给了迭代器区间构造,可以给一个区间,用这些值构造堆
这里还可以换成deque,不过是不太好的,因为deque的随机访问要计算的,不如vector
下面我们模拟实现一下优先级队列
namespace bai {
template>
class priority_queue
{
private:
void AdjustDown(int parent)
{
int child = parent * 2 + 1;
while (child < _con.size())
{
if (child + 1 < _con.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 (_con[child] > _con[parent])
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
public:
template
priority_queue(InputIterator first, InputIterator last)
{
while (first != last)
{
_con.push_back(*first);
++first;
}
//建堆
for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(i);
}
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
void push(const T& x)
{
_con.push_back(x);
AdjustUp(_con.size() - 1);
}
const T& top()
{
return _con[0];
}
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
Container _con;
};
}
这里逻辑其实都不难,用到了以前我们堆的知识,如果对堆还不够理解的同学可以看我之前对堆的详解
(20条消息) 堆及其多种接口与堆排序的实现_KLZUQ的博客-CSDN博客
我们测试一下,没有问题
仿函数,也叫做函数对象
如果不看其他的,只看cout输出这里,我们会认为lessfunc是一个函数
实际上这个类重载了括号,本质等价于lessfunc.operator()
本质是把这个类的对象像函数一样使用,作用是替代C语言里的函数指针
我们将它变为类模板,使用范围就变的很广阔了
那仿函数到底是干什么的呢?
像我们这样的比较,是写死的 ,我们可以用函数指针来让它变得灵活,但C++不使用函数指针,因为函数指针的可读性很差,所以就开发出了仿函数
我们把这些先全改为<的比较,然后使用com来进行控制
这样我们的比较就变得灵活了,传入less或者greater,就可以切换大于和小于的比较,从而调整我们是要建大堆,还是建小堆 ,我们可以根据需求写出自己的less和greater,对各种类型都可以进行比较
简单来说,仿函数就是通过模板参数,调用比较类型,调用operator(),operator()是我们自己实现的,或者我们用库里面的less或者greater也可以
我们把之前写过的Date类拿过来,都进行排序
有些场景下我们需要自己写仿函数
这个情况非常诡异,每一次运行结果都不一样
因为我们的类型是一个指针,new出来的地址大小是不确定的,带有随机性,所以会有这种结果
我们写一个这样的仿函数,就可以解决问题
我们可以通过仿函数来控制比较规则,而不是被优先级队列给写死了
//stack.h
using namespace std;
#include
#include
#include
namespace bai
{
//容器适配器
template>
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;
};
void test() {
stack st1;
st1.push(1);
st1.push(2);
st1.push(3);
st1.push(4);
while (! st1.empty()) {
cout << st1.top() << " ";
st1.pop();
}
cout << endl;
stack> st2;
st2.push(10);
st2.push(20);
st2.push(30);
st2.push(40);
while (!st2.empty()) {
cout << st2.top() << " ";
st2.pop();
}
cout << endl;
}
}
//queue.h
#include
#include
#include
using namespace std;
namespace bai
{
//容器适配器
template>
class queue
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
//_con.pop_front();
_con.erase(_con.begin());
}
T& front()
{
return _con.front();
}
T& back()
{
return _con.back();
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
void test_queue() {
//std::queue> q;
queue> q2;
q2.push(10);
q2.push(20);
q2.push(30);
q2.push(40);
while (!q2.empty()) {
cout << q2.front() << " ";
q2.pop();
}
cout << endl;
}
}
//priority_queue.h
using namespace std;
#include
#include
namespace bai {
template,class Comapre = less>
class priority_queue
{
private:
void AdjustDown(int parent)
{
Comapre com;
size_t child = parent * 2 + 1;
while (child < _con.size())
{
if (child + 1 < _con.size() && com(_con[child] , _con[child + 1]))
{
++child;
}
if (com(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void AdjustUp(int child)
{
Comapre com;
int parent = (child - 1) / 2;
while (child > 0)
{
if (com(_con[parent] , _con[child]))
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
public:
priority_queue()
{}
template
priority_queue(InputIterator first, InputIterator last)
{
while (first != last)
{
_con.push_back(*first);
++first;
}
//建堆
for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(i);
}
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
void push(const T& x)
{
_con.push_back(x);
AdjustUp(_con.size() - 1);
}
const T& top()
{
return _con[0];
}
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
Container _con;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d)const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
bool operator>(const Date& d)const
{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
friend ostream& operator<<(ostream& _cout, const Date& d);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
struct LessPDate
{
bool operator()(const Date* p1, const Date* p2)
{
return *p1 < *p2;
}
};
void test() {
priority_queue,LessPDate> pq;
pq.push(new Date(2021, 7, 28));
pq.push(new Date(2021, 6, 28));
pq.push(new Date(2021, 8, 28));
while (!pq.empty()) {
cout << *pq.top() << " ";
pq.pop();
}
cout << endl;
}
}
以上即为本期全部内容,希望大家可以有所收获
如有错误,还请指正