容器适配器
- 容器适配器是一种设计模式,它通过封装现有的序列容器类,并重定义其成员函数,以提供不同的公共或满足特定的需求。这种适配器类似于电源适配器,它将不兼容的电源(如不同国家的电压标准)转换成可用的电源(即适合电器使用的电压),以便用户能够方便的是同各种电器。
- 在计算机编程中,容器适配器运去程序员使用已经存在的序列容器类(如vector、deque、list等),同时获得预定义的特定功能,如stack实现后进先出(LIFO)存储、queue实现先进先出(FIFO)存储、已经priority_queue实现基于优先级的存储。
- 容器适配器本质上是容器的一种变体,它利用了其他基础容器模板类中已经实现的成员函数,并在必要时添加或创新自己的成员函数。
- stack 栈
- queue 队列
- priority_queue 优先级队列(堆)
这些容器有一个特性,都是使用线性表作为底层容器。所以,对于已经有满足条件的容器可以充当这个容器时,可以采用对象之间的组合封装进行合理使用。
当然,这是大范围和小范围的概念,例如stack和queue都可以使用vector和list封装,因为list和vector所支持的功能大于stack和queue所需要的功能。
stack
stack介绍
1. stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
2. stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
3. stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
empty:判空操作
back:获取尾部元素操作
push_back:尾部插入元素操作
pop_back:尾部删除元素操作
4. 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque。
stack的使用
std::stack<int> myStack;
创建栈堆对象:使用std::stack类创建一个堆栈对象,需要指定栈堆中元素的类型
myStack.push(10);
myStack.push(20);
myStack.push(30);
压入元素:使用push()函数将元素压入栈堆的顶部。
myStack.pop();
弹出元素:使用pop()函数从堆栈的顶部移除元素。你可以使用循环或条件语句来连续弹出元素
int topElement = myStack.top();
访问顶部元素:使用top()函数可以访问堆栈顶部的元素,但不会将其从堆栈中移除
if (myStack.empty()) {
// 堆栈为空
} else {
// 堆栈不为空
}
检查堆栈是否为空:使用empty()函数可以检查堆栈是否为空。如果堆栈为空,返回true,否则返回false。
int stackSize = myStack.size();
获取堆栈的大小:使用size()函数可以获取堆栈中元素的数量。
stack模拟实现
#include<iostream>
#include<deque>
namespace my_stack
{
//stack是一个适配器模式,可以借助其他容器实现
template<class T, class Container = std::deque<T>>//
class stack
{
typedef stack<T, Container> _stack; //stack对象类型
public:
stack()
:_con(Container())
{}
//尾插
void push(const T& val) { _con.push_back(val); }
//尾删
void pop() { _con.pop_back(); }
//取栈顶数据
T& top() { return _con.back(); }
const T& top() const { return _con.back(); }
//数据个数
size_t size() const { return _con.size(); }
//是否为空
bool empty()const { return _con.empty(); }
//交换
void swap(_stack& _s) { _con.swap(_s._con); };
private:
Container _con; //底层容器对象
};
}
queue
queue介绍
- 先进先出(FIFO):std::queue是一种先进先出的数据结构,这意味着兑现添加到队列的元素将首先被移除。
- 基于容器的适配器:std::queue是基于容器的适配器,它使用底层容器来存储元素。
- 快插入和删除:忧郁std::queue是基于容器的适配器,它使用底层容器的插入和删除操作来实现元素的添加和移除。这些操作的时间复杂度通常是常熟时间,因此插入和删除操作非常高效。
- 无索引访问:std::queue不支持通过索引访问元素,只能访问队列的前段和末尾元素,即使用front()和back()函数。
- 无迭代器支持:std::queue不支持迭代器。如果你需要遍历队列中的元素,你需要先将它们移除。
- 大小可变:std::queue的大小是可变的,你可以根据需要动态地添加和移除元素。
queue的使用
std::queue<int> myQueue;
创建队列对象:使用std::queue类创建一个队列对象。需要指定队列中元素的类型。
myQueue.push(10);
myQueue.push(20);
myQueue.push(30);
添加元素:使用push()函数将元素添加到队列的末尾。也可以连续调用push()函数来添加多个元素。
myQueue.pop();
移除元素:使用pop()函数从队列的前端移除元素。可以使用循环或条件语句来连续移除元素。
int frontElement = myQueue.front();
int backElement = myQueue.back();
访问前端和末尾元素:使用front()函数可以访问队列的前端元素,使用back()函数可以访问队列的末尾元素,但不会将它们从队列中移除。
if (myQueue.empty()) {
// 队列为空
} else {
// 队列不为空
}
检查队列是否为空:使用empty()函数可以检查队列是否为空。如果队列为空,返回true,否则返回false。
int queueSize = myQueue.size();
获取队列的大小:使用size()函数可以获取队列中元素的数量。
queue模拟实现
#include <deque>
namespace qx
{
template<class T, class Container = std::deque<T>>
class queue {
public:
//队尾入队列
void push(const T &x) {
_con.push_back(x);
}
//队头出队列
void pop() {
_con.pop_front();
}
//获取队头元素
T &front() {
return _con.front();
}
const T &front() const {
return _con.front();
}
//获取队尾元素
T &back() {
return _con.back();
}
const T &back() const {
return _con.back();
}
//获取队列中有效元素个数
size_t size() const {
return _con.size();
}
//判断队列是否为空
bool empty() const {
return _con.empty();
}
//交换两个队列中的数据
void swap(queue<T, Container> &q) {
_con.swap(q._con);
}
private:
Container _con;
};
}// namespace qx
priority_queue 优先级队列
priority_queue介绍
- std::priority_queue 用于实现优先队列数据结构。优先队列是一种特殊的队列,其中的元素按照一定的优先级顺序进行排列,而不是按照插入的顺序。
- std::priority_queue 使用堆(heap)数据结构来维护元素的顺序,通常用于需要按照一定规则获取最高优先级元素的场景。在默认情况下,std::priority_queue 会以降序排列元素,即最大的元素会被放置在队列的前面。你也可以通过提供自定义的比较函数来改变排序顺序。
priority_queue的使用
默认构造:
std::priority_queue<T> pq; // 创建一个空的优先队列,T 是元素类型
带有比较函数的构造函数:
std::priority_queue<T, Container, Compare> pq;
- T是元素类型。
- container是底层容器类型,默认情况下使用std::vector。
- compare是比较函数类型,用于确定元素的顺序,默认情况下,使用operator<来比较元素。
#include <iostream>
#include <queue>
int main() {
// 构造一个默认的优先队列,元素类型为 int,使用默认比较函数 operator<
std::priority_queue<int> pq1;
// 构造一个优先队列,元素类型为 int,使用自定义的比较函数 greater,实现最小堆
std::priority_queue<int, std::vector<int>, std::greater<int>> pq2;
// 构造一个优先队列,元素类型为 std::string,使用默认比较函数 operator<
std::priority_queue<std::string> pq3;
// 构造一个优先队列,元素类型为 double,使用 lambda 表达式作为比较函数,实现最大堆
auto cmp = [](double a, double b) { return a < b; };
std::priority_queue<double, std::vector<double>, decltype(cmp)> pq4(cmp);
return 0;
}
pq.push(value); // 插入元素 value
向优先队列中插入元素。
pq.pop(); // 移除队列顶部元素
移除队列顶部的元素(即最高优先级元素)。
T highestPriority = pq.top(); // 获取最高优先级元素
获取队列顶部的元素(即最高优先级元素)。
T highestPriority = pq.top(); // 获取最高优先级元素
获取队列顶部的元素(即最高优先级元素)。
bool isEmpty = pq.empty(); // 如果队列为空则返回 true,否则返回 false
检查队列是否为空。
int numElements = pq.size(); // 获取队列中的元素数量
获取队列中的元素数量。
自由比较函数:
struct MyComparator {
bool operator()(const T& a, const T& b) {
// 自定义比较逻辑
}
};
std::priority_queue<T, Container, MyComparator> pq;
可以通过提供自定义的比较函数来改变元素的排序顺序。比较函数应该返回
true
,如果第一个参数应该排在第二个参数之前
priority_queue模拟实现
priority_queue的底层实际上就是堆结构,实现priority_queue之前,我们先认识两个重要的堆算法。(下面这两种算法我们均以大堆为例)
以大堆为例,堆的向上调整算法就是在大堆的末尾插入一个数据后,经过一系列的调整,使其仍然是一个大堆。
调整的基本思想:
1、将目标结点与其父结点进行比较。
2、若目标结点的值比父结点的值大,则交换目标结点与其父结点的位置,并将原目标结点的父结点当作新的目标结点继续进行向上调整;若目标结点的值比其父结点的值小,则停止向上调整,此时该树已经是大堆了。
例如,现在在大堆的末尾插入数据88。
先将88与其父结点54进行比较,发现88比其父结点大,则交换父子结点的数据,并继续进行向上调整。
此时将88与其父结点87进行比较,发现88还是比其父结点大,则继续交换父子结点的数据,并继续进行向上调整。
这时再将88与其父结点89进行比较,发现88比其父结点小,则停止向上调整,此时该树已经就是大堆了。
向上调整算法:
void AdjustUp(int child)
{
int parent = (child - 1) / 2;
while (child > 0) {
// 默认less ,也就是parent<child
if (_comp(_con[parent], _con[child]))// 通过所给比较方式确定是否需要交换结点位置
{
// 将父结点与孩子结点交换
swap(_con[child], _con[parent]);
// 继续向上进行调整
child = parent;
parent = (child - 1) / 2;
} else// 已成堆
{
break;
}
}
}
调整的基本思想如下:
1、将目标结点与其较大的子结点进行比较。
2、若目标结点的值比其较大的子结点的值小,则交换目标结点与其较大的子结点的位置,并将原目标结点的较大子结点当作新的目标结点继续进行向下调整;若目标结点的值比其较大子结点的值大,则停止向下调整,此时该树已经是大堆了。
将该二叉树从根结点开始进行向下调整。(此时根结点的左右子树已经是大堆)
将60与其较大的子结点88进行比较,发现60比其较大的子结点小,则交换这两个结点的数据,并继续进行向下调整。
此时再将60与其较大的子结点87进行比较,发现60比其较大的子结点小,则再交换这两个结点的数据,并继续进行向下调整。
这时再将60与其较大的子结点54进行比较,发现60比其较大的子结点大,则停止向下调整,此时该树已经就是大堆了。
向下调整算法:
void AdjustDown(int n, int parent)
{
int child = 2 * parent + 1;
while (child < n) {
//_comp(_con[child], _con[child + 1])表示child<child+1
if (child + 1 < n && _comp(_con[child], _con[child + 1])) {
child++;
}
// parent<child
if (_comp(_con[parent], _con[child]))// 通过所给比较方式确定是否需要交换结点位置
{
// 将父结点与孩子结点交换
swap(_con[child], _con[parent]);
// 继续向下进行调整
parent = child;
child = 2 * parent + 1;
} else// 已成堆
{
break;
}
}
}
模拟实现:
// 优先级队列使用vector作为其底层存储数据的容器,priority_queue就是堆
// 默认情况是大堆
#include <iostream>
#include <vector>
using namespace std;
namespace phw {
// 仿函数less(使内部结构为大堆)
template<class T>
struct less {
bool operator()(const T &x, const T &y) {
return x < y;
}
};
// 仿函数greater(使内部结构为小堆)
template<class T>
struct greater {
bool operator()(const T &x, const T &y) {
return x > y;
}
};
// 优先级队列
template<class T, class Container = vector<T>, class Compare = greater<T>>
class priority_queue {
public:
// 堆的向上调整
void AdjustUp(int child) {
int parent = (child - 1) / 2;
while (child > 0) {
// 默认less ,也就是parent<child
if (_comp(_con[parent], _con[child]))// 通过所给比较方式确定是否需要交换结点位置
{
// 将父结点与孩子结点交换
swap(_con[child], _con[parent]);
// 继续向上进行调整
child = parent;
parent = (child - 1) / 2;
} else// 已成堆
{
break;
}
}
}
// 堆的向下调整
void AdjustDown(int n, int parent) {
int child = 2 * parent + 1;
while (child < n) {
//_comp(_con[child], _con[child + 1])表示child<child+1
if (child + 1 < n && _comp(_con[child], _con[child + 1])) {
child++;
}
// parent<child
if (_comp(_con[parent], _con[child]))// 通过所给比较方式确定是否需要交换结点位置
{
// 将父结点与孩子结点交换
swap(_con[child], _con[parent]);
// 继续向下进行调整
parent = child;
child = 2 * parent + 1;
} else// 已成堆
{
break;
}
}
}
// 插入元素到队尾(并排序)
void push(const T &x) {
_con.push_back(x);
AdjustUp(_con.size() - 1);// 将最后一个元素进行一次向上调整
}
// 弹出队头元素(堆顶元素)
void pop() {
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(_con.size(), 0);// 将第0个元素进行一次向下调整
}
// 访问队头元素(堆顶元素)
T &top() {
return _con[0];
}
const T &top() const {
return _con[0];
}
// 获取队列中有效元素个数
size_t size() const {
return _con.size();
}
// 判断队列是否为空
bool empty() const {
return _con.empty();
}
private:
Container _con;// vector容器
Compare _comp; // 比较方式
};
}// namespace phw
(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(_con.size(), 0);// 将第0个元素进行一次向下调整
}
// 访问队头元素(堆顶元素)
T &top() {
return _con[0];
}
const T &top() const {
return _con[0];
}
// 获取队列中有效元素个数
size_t size() const {
return _con.size();
}
// 判断队列是否为空
bool empty() const {
return _con.empty();
}
private:
Container _con;// vector容器
Compare _comp; // 比较方式
};
}// namespace phw
仿函数
- 仿函数,就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了;
- 有了仿函数类过后,再结合模板使用,就可以使一些需要重复使用的代码独立出来,以便下次复用,这样有利于资源的管理(这点可能是它相对于函数最显著的优点了)。
- 就比如下面我们所需要实现的优先队列,默认的是大堆(如图,库里面是反着的,less是大堆,greater是小堆),当我们想要小堆的时候,就需要用仿函数,这样只需要我们再使用优先级队列时改变传的模板参数即可。
deque
deque介绍
- deque(双端队列):是一种双开口的“连续”空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素,与list比较,空间利用率比较高。
- 所说的来连续,并不是说都是连续的,在底层,deque是一段一段的数组,数组中存储的数据,然后有一个指针数组存储每一个数组的地址,这样,与vector比较,头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。
- 与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
但是有一个致命的缺陷是:中间插入和删除效率与下标随机访问的效率不高,这个缺陷是底层物理结构所导致的。
为什么选择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不仅效率高,而且内存使用率高。
结合了deque的优点,而完美的避开了其缺陷。
器适配器的学习对类和对象的封装又有了进一步的认识,类和类之间的组合应用有了初步的了解,最后对deque这个适配器容器的底层容器,了解了其底层结构的复杂和优缺点。