【C++】STL——Stack&Queue(容器适配器) - 详解
本文将讲解容器适配器的相关知识。我将从stack和queue这两个基础数据结构引出适配器的概念和相关知识。
1.stack
stack这个数据结构我们以前介绍过,在这里就不再过多赘述。主要讲一下在C++中,关于stack的一些基础使用和自己模拟实现C++中的stack。
1.1stack的基础使用
函数说明 | 接口说明 |
---|---|
stack() | 构造空的栈 |
empty() | 检测stack 是否为空 |
size() | 返回stack 中元素的个数 |
top() | 返回栈顶元素的引用 |
push() | 将元素val 压入stack 中 |
pop() | 将stack 中尾部的元素弹出 |
表中就是C++中关于stack的一些基础使用,如果想要加强练习可以通过栈的相关算法题训练加强自己的理解。
1.2stack的模拟实现
#include
namespace popo
{
templaste
class stack{
public:
stack() {}
void push(const T&x) {_c.push_back()x;}
void pop() {_c,pop_bck();}
const T& void top()const {retuen _.back();}
T& pop() {_c.back();}
size_t size() {retun _c.sizeO();}
bool empty() {return _c.empty();}
private:
std::vector _c;
};
}
从stack的模拟实现来看,栈其实是一种特殊的vector,因此vector完全可以模拟实现stack。这也将为我们后面讲解容器适配器做了铺垫。
2.queue
queue队列这个数据结构同样曾经学过,不做过多赘述,直接讲解基础使用和模拟实现。
2.1queue的基础使用
函数声明 | 接口说明 |
---|---|
queue() | 构造空的队列 |
empty() | 检测队列是否为空,是返回 true,否则返回 false |
size() | 返回队列中有有效元素的个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素 val 入队列 |
pop() | 将队头元素出队列 |
大家可以借助一道题熟悉queue的基本操作,225. 用队列实现栈 - 力扣(LeetCode)
2.2queue的模拟实现
#include
namespace popo {
template
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:
std::list _c;
};
}
在C++中,stack和queue都是非常重要的容器适配器,它们基于其他容器实现,为数据的存储和访问提供了特定的规则,通过灵活运用stack和queue我们可以方便的解决许多涉及数据有序存储和访问的编程问题。
3. priority_queue
priority_queue名为优先队列,也是一种容器适配器,根据严格的弱排序标准,它的第一个元素是他所包含的元素中最大的。本质上是一个堆,默认是大堆。
操作特性:插入元素时会自动调整堆结构以维持优先级;取出元素时只能访问或删除优先级最高的元素(队头)。
3.1 模板声明:
#include // 必须包含的头文件
// 完整模板声明
template , // 底层容器类型(默认vector)
class Compare = less // 比较器(默认大顶堆)
> class priority_queue;
注意第一个参数是元素类型;第二个参数是底层容器(默认是vector);第三个参数是比较器(默认是less< int >,即大堆,greater<int>表示为小堆),本质是一个仿函数。
3.2 与queue的区别
特性 | queue | priority_queue |
---|---|---|
数据结构 | 队列(FIFO) | 堆(按优先级排序) |
元素取出顺序 | 先进先出 | 优先级最高的先出 |
访问方式 | 可访问队头和队尾 | 只能访问优先级最高元素 |
底层默认容器 | deque | vector |
3.3 总结
priority_queue优先队列通过堆结构实现了元素的自动排序,核心优势是能高效获取和删除优先级最高的元素(时间复杂度O(log n))。它适用于需要动态处理优先级任务的场景,使用时需注意默认是最大堆,如需小堆需显式指定比较器。
4.容器适配器
适配器是一种设计模式,该模式就是将一个类的接口转换成用户希望的另一个接口,类比生活中交流电适配器(适配不同地区电压不同的设备)。
4.1 STL中stack和queue的底层结构
stack声明:template <class T,class Container = deque<T>> class stack;
queue声明:template<class T,class Container = deque<T>> class queue;
虽然stack和queue可以存放元素,但是他俩并不属于容器,而是容器适配器,这是因为stack和queue知识对其他容器的接口进行了封装,STL中stack和queue默认使用deque作为底层容器,前面说的priority queue则默认使用vector作为底层容器。
4.2工作原理
底层容器:每种容器适配器都可以选择一种或几种标准容器作为其底层容器以实现功能。
接口封装:容器适配器会根据自身数据结构的特性,对底层容器的接口进行重新封装。
5.deque
deque也就是双端队列,是一种双开口的“连续空间”的数据结构。它是一种序列式容器,和vector类似,都可以存储一组相同类型的元素。但与vector不同的是,deque允许在序列的两端进行插入和删除操作,而vector在头部插入和删除元素的效率极低,因为它需要移动大量元素来保持数据连续性。
5.1底层实现
deque并不是真正连续的空间,而是一段段连续的小空间拼接而成,类似于一个动态的二维数组,底层结构如下:
这张图反应了deque的底层结构, 主要是数据块数组和映射表两部分构成。
映射表每个元素就是一个指针,指向一个数据块,通过映射表。deque可以快速定位到特定位置的元素所在的数据块。
数据块数组则是一段连续的空间,用于存储数据,这些数据块之间在物理内存上不一定连续。
5.2 deque的缺点
deque虽然相比vector和list,,结合了两个的优点。不过它有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器需要频繁的去检测其是否移动到某段数据块空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多。
5.3 选择deque作为底层容器的原因
stack和queue的需求都是在一端或者两端进行数据操作,分别遵循LIFO和FIFO原则。vector的头部操作效率低,扩容成本高;list的随机访问效率低,内存开销大。而deque对头部和尾部数据的操作时间复杂度均为O(1),无需移动大量元素,效率极高。完美匹配stack和queue的核心操作需求。C++也允许通过模板参数指定其他容器作为底层,但deque时最通用且高效的默认选择。
5.4 STL标准库中stack和queue的模拟实现
stack:
#include
namespace popo
{
template>
//template>
//template>
class stack
{
public:
stack() {}
void push(const T& x) {_c.push_back(x);}
void pop() {_c.pop_back();}
T& top() {return _c.back();}
const T& top()const {return _c.back();}
size_t size()const {return _c.size();}
bool empty()const {return _c.empty();}
private:
Con _c;
};
}
queue:
#include
#include
namespace popo
{
template>
//template>
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;
}