📕 容器适配器介绍
概念
适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。
比如 stack 和 queue ,这两个容器是在其他容器的基础上实现的,对其他容器的接口进行封装处理,进而实现stack 和 queue 的功能。从下面 C++ 手册中的截图,可以看出 stack、queue默认是用 deque(双端队列)实现的,它们并不是用数组或者链表实现,而是直接使用已有的容器。
deque 的简单介绍
deque,双端队列,既有数组的优点(支持随机访问),又具备链表的优点(插入删除效率高),其底层实现如下图。
首先,有一个指针数组(该数组又被称作中控器),该数组里面存放的元素的数据类型的是指针,每一个指针指向一段连续的空间,每一段空间的大小都是固定的。要访问数据,首先要在 map 数组里面找到数组里的元素(指针),例如 map[4],代表数组里第五个数据,这是一个指针,然后通过指针访问其指向的空间,这块空间里面存储的才是数据。
那么如何实现迭代器呢?如下图所示,迭代器 iterator 里面有四个指针,分别是 cur,first,last,node。有first 、last 这是因为中控器的每一个元素(指针),指向的空间都是不一样的,必须要设置 first、last 指针来防止越界访问。至于 node 就更好理解了,这是为了只要当前访问的是中控器里的哪一个元素。
至于其插入数据,也不难理解。双端队列,顾名思义,可以在两端进行插入、删除操作。如下图,在队头插入的时候,直接插入到 start 迭代器的 cur 指针的前一个位置即可,如果 start 迭代器指向的这个数组满了,那么就开辟一块新空间,让中控器(map数组)里面上一个元素(指针)指向这块空间,再将数据插入这一块新空间。尾插同理。(map数组是从中间开始使用的,就是为了方便头插)
虽然 deque 听起来高大上,兼具链表和顺序表的优点,但是实际应用的时候使用双端队列的情况却并不多,所以简单了解即可。
📕 stack
如下,使用 vector、list ,对其接口进行简单封装(使用到的接口,在 vector 、list 里面都是一同名的),就成了stack。但是为了减少重复冗余,这里将底层容器(vector、list )传模板参数,使用的时候写出来是用 vector 还是 list 就行就行。
#pragma once
#include<iostream>
#include<vector>
#include<list>
using namespace std;
namespace simulate
{
template<class T, class Container = vector<T>>
class stack
{
public:
void push(const T& val)
{
_con.push_back(val);
}
void pop()
{
_con.pop_back();
}
const T& top()
{
return _con.back();
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
}
📕 queue
queue 和 stack 类似,也是使用 list 和 vector 实现,但是 queue 默认是用 list 会更好一点,因为频繁的出队,本质上是在头删,对于 vector 而言,开销较大。
#pragma once
#include<iostream>
#include<vector>
#include<list>
using namespace std;
namespace simulate
{
template<class T, class Container = list<T>>
class queue
{
public:
void push(const T& val)
{
_con.push_back(val);
}
void pop()
{
_con.pop_front();
}
const T& top()
{
return _con.front();
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
}
📕 priority_queue
优先级队列是一种容器适配器,根据一些严格的弱排序标准,专门设计使其第一个元素始终是它包含的最大元素。
简单地说,priority_queue 就是类似于堆,可以有大堆、小堆,用哪个完全取决于自己。利用priority_queue 获取元素的时候,只能获取堆顶元素,也就是优先级队列的“优先级最高”的元素。
可是,如何判别容器里的元素,谁的优先级更高呢?本质上其实还是用大于、小于号去比较,如果 priority_queue< int > ,容器里的元素是 int 类型的,就是根据 int 数据的大小;如果是 priority_queue< string > ,那么容器里的元素就是 stirng 类型的,无疑, string 类型里面是重载了 > 、 < 符号的,所以,只需要直接调用这两个重载即可,至于其内部如何实现,根据什么来判断string 类的对象的大小,我们并不关心。再假设,如果是自定义类型,就要自己实现大于、小于的重载, 根据什么来判断 “优先级” 也就是自己定义了。
如下,以向上调整算法为例,判断优先级那里本质上就是对元素进行大小比较。
void adjust_up(int child)
{
Comapre com;
int parent = (child - 1) / 2;
while (child > 0)
{
if (_con[parent] < _con[child]) // 判断优先级
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
根据堆的存储结构,这里使用 vector 进行封装无疑更加适合。同时,向下调整和向上调整两个算法都需要自己实现,并不困难。(可以参考这篇文章:堆的实现)
但是,由于要控制底层是大堆还是小堆,所以又需要传模板参数(默认是大堆),将这个参数叫做 Compare 。这里使用仿函数
,通过其返回值来判断是实现大堆还是小堆,仿函数本质上是一个类,但是可以像函数一样使用它,这是因为重载了 operator()(const T& x,const T& y) 。实例化出一个对象com之后,可以直接 com(x,y) ,调用了 operator()(const T& x,const T& y) 这个方法。
如下,原本的 _con[parent] < _con[child] ,变成了 com( _con[parent] , _con[child] ) 。而 com 是模板类 Compare 实例化出的一个对象,根据传输的参数,就可以实现大堆还是小堆。
#pragma once
#include<vector>
using namespace std;
namespace simulate
{
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 = vector<T>, class Comapre = less<T>>
class priority_queue
{
public:
void adjust_up(int child)
{
Comapre com;
int parent = (child - 1) / 2;
while (child > 0)
{
//if (_con[parent] < _con[child])
if (com(_con[parent], _con[child]))
//if (Comapre()(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void adjust_down(int parent)
{
size_t child = parent * 2 + 1;
while (child < _con.size())
{
Comapre com;
//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[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void push(const T& x)
{
_con.push_back(x);
adjust_up(_con.size() - 1);
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
adjust_down(0);
}
const T& top()
{
return _con[0];
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
}
入队,就是把数据放到堆的末尾,然后对其向上调整;出队,就是将堆顶元素和堆尾元素交换,然后将堆顶元素向下调整。这和堆的操作是一摸一样的,因为优先级队列底层就是一个堆。