1. 容器适配器
在之前的学习中,我们应该已经学习过了栈和队列这两个数据结构,而在C++的STL库中同样有这两个数据结构。这两个数据结构被称为容器适配器。
那么容器适配器是什么?我们之前学的vector、list等存放数据的就称之为容器,而 stack 和queue 叫做他们的适配器,也就是这两个数据结构是兼容我们之前所学的容器的。如果我们到cplusplus的官网查看这个类的模板参数,我们就会发现它的模板参数其中之一是一个容器的缺省值,这个缺省值给的是一个叫 deque 的容器(我们稍后再介绍deque)。
这说明queue和stack这两个STL的数据结构是可以兼容很多我们已经学过以及以后要学的容器的。所以如果我们需要在C++中使用 stack 或 queue 的时候,我们就可以直接用库里已经实现好的文件。
2. deque
2.1 介绍
说了这么多,那么deque是什么呢?deque被称为双端队列,queue只允许在数组的一端进行插入或删除数据。我们先暂时把deque看做成一个数组,我们可以在这个数组的两端进行插入和删除数据,也可以在任意位置进行插入或删除数据。deque 也支持随机存储,所以我们在访问deque的时候,同样可以用 “ [ ] ” 来进行访问。我们要使用它的时候要包含<deque>的头文件。
2.2 底层原理
我们当时模拟实现 vector 的时候,是用三个迭代器指向了一块空间的三个地方,不够的时候随时扩容。但是 deque 的底层却不是用迭代器实现的,而是用一个中控数组(在源码里叫map , 只是名字叫map ,和以后学的数据结构没有关系)。鉴于 deque 的特殊性,我们是可以在 deque 的头或尾两端插入,我们开辟空间总不会向前开辟。所以deque的实现方法是用一个中控数组存储所有的数组,这个中控数组也就是一个数组的指针数组。并且第一个数组会尽可能的放在中间的位置。这是因为我们会在两端随机进行操作,所以在前后都留出位置方便我们尽量的少扩容。实现的时候保证每个数组都是一样大的,这样会方便我们随机读取数据。当中控数组满的时候,我们会对中控数组进行扩容,而成员数组只需要储存到新的中控数组中就可以了。
存储数据的方法都这样了,那么迭代器也肯定不会简单。deque 的迭代器也是被封装起来的。deque 的迭代器总共有四个成员变量,分别是:cur(指向当前读取的数据) 、first(每个小数组的第一个位置) 、last(每个小数组的最后一个位置) 、node(当前是哪一个小数组)
图中的 begin 迭代器的cur指向deque的当前元素,因为我们这里是要第一个元素,所以cur指向了第一个元素,first 指向数组开头的位置,last指向数组最后的位置,依旧是左闭右开,当cur指到了last的位置说明这个数组已经被遍历完了。node负责指向每一个数组,当一个数组的迭代器走到末尾的时候,node就会去寻找新的数组,并且更新其他三个变量。
end 迭代器和 begin 类似,只是因为 end 指向的是最后一个元素的位置,所以end迭代器的cur会指向最后一个元素。
同样的,我们对迭代器进行实现的时候也要重载 ++、+= 这些运算符。因为deque的迭代器是封装起来的,用于返回的其实只有 cur ,我们在重载运算符的时候,其他的三个变量都是辅助cur来寻找新的位置的。
2.3 使用deque模拟实现栈和队列
上面我们已经学习了deque,现在我们就可以用deque来模拟实现一下栈和队列了。实现的时候要记得放在自己自定义的命名空间内,这样在测试的时候才不会调到STL库中的函数。
我们先熟悉一下deque的成员函数,以便我们一会实现。这里我们只看一些一会会用到的成员函数。
push_back 尾插 、push_front 头插 、pop_back 尾删 、 pop_front 头删 、“ [ ] ”的重载 、size 数组的大小 、empty 数组是否为空
实现栈的时候,我们要记得栈是后进先出。由于我们是使用双端队列,无论是使用哪一边的push和pop都是可以的,但是要注意栈只能从一边进行插入删除,所以我们实现栈的时候push 和 pop 的方向要保持一致。
template<class T, class Con = deque<T>>
class stack
{
public:
stack(){}
void push(const T& x) {
_c.push_front(x);
}
void pop() {
_c.pop_front();
}
T& top() {
return _c.front();
}
const T& top()const {
return _c.front();
}
size_t size()const {
return _c.size();
}
bool empty()const {
return _c.empty();
}
private:
Con _c;
};
实现队列的时候要注意,队列只支持一边进另一边出,也就是先进先出,所以进的那边不能出元素,push 和 pop 的方向就应该完全相反。
template<class T, class Con = deque<T>>
class queue
{
public:
queue() {};
void push(const T& x) {
_c.push_back(x);
}
void pop() {
_c.pop_front();
}
T& back() {
return _c.back();
}
const T& back()const {
return _c.back();
}
T& front() {
return _c.front();
}
const T& front()const {
return _c.front();
}
size_t size()const {
return _c.size();
}
bool empty()const {
return _c.empty();
}
private:
Con _c;
};
2.4 按需实例化
我们实现这种模板类的时候,如果在模板参数中有容器,由于每个容器支持的成员函数都是不一样的。比如vector支持了 [ ] 的重载,但是list就没有支持。这个时候,如果我们调用类里面的成员函数的时候,参数传的是list ,但是却用了 [ ] 的随机存储。只要我们不去调用这个函数,编译器就不会报错。只有我们调用这个函数的时候,编译器才会告诉我们,list是不支持 [ ] 的重载的,这个就叫做按需实例化。也就是编译器只会实例化调用的函数,没有调用的函数如果没有明显的语法问题,编译器是不会报错的。
在下面的例子中, 我们可以看到,同样我们传的容器都是list,我们只调用 func2 的时候是可以成功运行的。但是我们一旦调用了 func 就会出现报错,原因是因为list没有重载 [ ] 运算符,无法使用 [ ] 来访问 list。
3. priority_queue
3.1 模拟实现
priority_queue 叫做优先级队列,也就是我们之前学过的堆。同样,它和之前的堆一样,也是一种数据结构。功能和我们以前学习过的堆是一模一样的。
我们依旧是先模拟实现一下优先级这个堆,我们先默认实现大根堆,也就是数组中最大的元素在第一个位置。具体细节在以前堆的讲解中都有。
template <class T, class Container = vector<T>>
class priority_queue
{
public:
priority_queue() {};
bool empty() const {
return _c.empty();
}
size_t size() const {
return _c.size();
}
T& top() const {
return _c[0];
}
void Adjustup(size_t child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (_c[parent] < _c[child])
{
swap(_c[parent], _c[child]);
child = parent;
parent = (parent - 1) / 2;
}
else {
break;
}
}
}
void push(const T& x) {
_c.push_back(x);
Adjustup(_c.size() - 1);
}
void AdjustDown(size_t parent)
{
int child = parent * 2 + 1;
while (child < _c.size())
{
if (child + 1 < _c.size() && _c[child] < _c[child + 1])
{
++child;
}
if (_c[parent] < _c[child])
{
swap(_c[parent], _c[child]);
parent = child;
child = child * 2 + 1;
}
else {
break;
}
}
}
void pop() {
swap(_c[0], _c[_c.size() - 1]);
_c.pop_back();
AdjustDown(0);
}
private:
Container _c;
};
3.2 仿函数
在实现了这个优先级队列后,这个堆默认是大根堆,但是如果我们想去再实现一个小根堆,如果再一模一样的去写一个,代码就太过于冗余了。这时我们引入一个叫仿函数的概念。这个仿函数是一个类,它的作用是通过重载 “ ( ) ” ,使得这个类的对象可以像函数一样被用来调用。
由于仿函数是一个类,所以仿函数也可以通过类模板的方式来进行书写。这里我们就直接 写一个比较大小的仿函数的类模板。
template<class T>
class Less
{
public:
bool operator()(const T& x , const T& y)
{
return x < y;
}
};
int main()
{
Less<int> LessFunc;
//下面两句代码意义是一样的 叫仿函数只是因为下面这一句看起来像函数调用
//但实际上是对象调用运算符重载
cout << LessFunc(1 , 2) << endl; // 结果为1(真)
cout << LessFunc.operator()(1 , 2) << endl; //这句代码是完整版
return 0;
}
所以我们在实现优先级队列的时候,也可以套用仿函数,在类模板的模板参数部分加上一个仿函数,这样我们就可以通过传递仿函数来改变优先级队列这个堆是大根堆还是小根堆了。
我们在成员变量的地方实例化了一个对象,所以在函数中我们可以直接使用仿函数。这里要注意,我们默认实现的是大根堆,也就是传Less的时候是大根堆,传Greater的时候才是小根堆。
template<class T>
class Less
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
template<class T>
class Greater
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
namespace my_ns
{
template <class T, class Container = vector<T>, class Compare = less<T>>
class priority_queue
{
public:
priority_queue() {};
bool empty() const {
return _c.empty();
}
size_t size() const {
return _c.size();
}
T top() const {
return _c[0];
}
void Adjustup(size_t child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
//此处使用仿函数
if (_comp(_c[parent] , _c[child]))
{
swap(_c[parent], _c[child]);
child = parent;
parent = (parent - 1) / 2;
}
else {
break;
}
}
}
void push(const T& x) {
_c.push_back(x);
Adjustup(_c.size() - 1);
}
void AdjustDown(size_t parent)
{
int child = parent * 2 + 1;
while (child < _c.size())
{
//此处使用仿函数
if (child + 1 < _c.size() && _comp(_c[child] ,_c[child + 1]))
{
++child;
}
//此处使用仿函数
if (com(_con[parent],_con[child]))
{
swap(_c[parent], _c[child]);
parent = child;
child = child * 2 + 1;
}
else {
break;
}
}
}
void pop() {
swap(_c[0], _c[_c.size() - 1]);
_c.pop_back();
AdjustDown(0);
}
private:
Container _c;
Compare _comp;
};
};