前言:在之前的章节里面我们学了STL库里面2个非常常用的容器:vector和list。有了前面的知识可以让我们更好的理解这部分内容——适配器。
适配器也是STL库里面6大组成部分之一
一、概念
- 什么是适配器?适配器有什么用?
在C++ STL中,适配器作为一种工具,可以将容器、迭代器或函数对象转换为另一种形式,从而提供新的接口或行为。STL中有三种常见的适配器:1. 容器适配器(Container Adapter)2. 迭代器适配器(Iterator Adapter)3. 函数适配器(Function Adapter)
我们先来先来学习容器适配器(Container Adapter)
容器适配器(Container Adapter)
(先看看这些名词有个印象,不理解看不懂也没关系,后面会详细学习介绍)
STL中有三种主要的容器适配器:
stack
:将任何底层容器(如deque
或vector
)适配为一个LIFO(后进先出)栈。默认使用deque
作为底层容器。(deque是一个容器)queue
:将底层容器适配为一个FIFO(先进先出)队列,默认使用deque
。priority_queue
:将底层容器适配为一个优先级队列,元素会根据优先级顺序出队。默认使用vector
。
此篇章只讲述stack和queue的使用,priority_queue在下一文。
容器适配器有什么用呢?
- 容器适配器将已有的容器类型改装成不同的抽象类型。
将已有的容器类型改装成不同的抽象类型是什么意思呢?
举个例子:
下面有一个list存储着数据(数据我任意填入),这里把list改装成stack类型,管理这些数据只需要使用stack中接口(成员函数)如:pop,push,就可以实现数据“先进先出”。
当然在此例子中如果把里面的容器list,换成vector也可以达到改装成stack效果。
二、适配器的使用
学习和使用过STL的容器:vector和list的接口函数的使用,在这里stack和queue接口函数的会非常的轻松愉快。但是这里有一些额外的知识需要进一步学习——deque的结构和使用。
在这个例子里面,代码如何实现呢?
#include <list>
#include <stack>
using namespace std;
int main()
{
stack<int, list<int>> List_ST;//创建一个stack,存int型数据,容器是list<int>
List_ST.push(0);
List_ST.push(10);
List_ST.push(20);
List_ST.push(30);
List_ST.push(40);
// 创建队列的副本
stack<int, list<int>> copyList_ST = List_ST;//为什么要先创建副本再打印呢?
//因为在适配器的使用中,我们最好不要直接跳过适配器去访问底层容器
// 使用 for 循环遍历副本stack并打印元素(后进先出)
cout << "List_ST:";
while (!copyList_ST.empty()) //如果stack不为空进入循环
{
cout << copyList_ST.top() << " "; //打印栈顶元素
copyList_ST.pop(); //打印完再中删除一个元素(头元素出)
}
cout << endl;
//栈弹出一个元素
List_ST.pop();
//再次打印
copyList_ST = List_ST;
cout << "List_ST:";
while (!copyList_ST.empty())
{
cout << copyList_ST.top() << " ";
copyList_ST.pop();
}
cout << endl;
return 0;
}
输出结果为:
List_ST:40 30 20 10 0
List_ST:30 20 10 0
2.1 stack
什么是stack(栈):
stack(栈)是一种数据结构,用于存储数据并遵循后进先出(LIFO, Last In, First Out)的原则。这意味着最后插入的元素会被最先移除。栈的数据结构可以类比为一叠盘子,新盘子总是放在最上面,取盘子时也总是从最上面开始取。
结合图形理解:
2.1.1.stack类模板和构造函数
参考网址:cplusplus.com/reference/stack/stack/
我们对照上面示例来分析一下stack各成员函数:
1.stack类模板
stack也是一个类,它通过类模板来实现对容器的适配。它的类模板定义大致如下:
template <class T, class Container = deque<T> >;
class stack{
... ...
};
T:数据类型模板,在此stack中你的数据类型如:int ,char,double
Container 翻译:容器:你需要的底层容器 如:list<T>,vector<T>,缺省值:默认传入deque<T>容器(下文会介绍这个容器)
注意:容器显式模板实例化 和 适配器类型需要一致!!否则编译报错(也就是2个T的实例化要一样,如下面2个int)
现在就能看懂示例代码中的显示实例化了:
stack<int, list<int>> List_ST;//创建一个stack,存int型数据,容器是list<int>
list是类名 ,而list<int>是一种类型了
以上代码执行时,实际上发生了:
T = int
:表示栈中存储的元素类型为int
。Container = list<int>
:表示使用std::list<int>
作为底层容器。
2.stack构造函数:
默认构造:
explicit stack(const Container& cont = Container());
- 功能:创建一个空栈,使用默认的底层容器(默认为
std::deque<T>
)进行构造。 - 参数:可以显式提供一个容器
cont
,否则使用默认构造的容器(通常为空)。 - 示例:
stack<int> defaultStack; // 使用默认容器 std::deque<int>
拷贝构造和复制构造,和vector类一样,这里就不多说了。
这里有一种适配器特有的构造:
从容器直接构造 :
explicit stack(const Container& cont);
- 功能:使用指定的容器
cont
构造一个栈。 - 参数:
cont
:可以是容器的常引用或右值引用。支持使用任何符合stack
要求的容器,如deque
、list
、vector
。 - 示例:
deque<int> deque = {1, 2, 3};
stack<int> Deque_ST(deque); // 使用 deque 构造 stack
2.1.2 stack的其他接口函数
在之前vector的学习中,这些函数的用法相信大家看一看就已经知道功能和用法,用法和vector类中的接口函数是一样的:
-
Constructor: 构造
stack
对象,可以使用默认构造函数,也可以基于其他容器进行构造。 -
empty(): 检查栈是否为空。如果栈为空,返回
true
;否则返回false
。 -
size(): 返回栈中元素的个数。
-
top(): 访问栈顶元素,但不移除该元素。
-
push(): 向栈顶插入一个元素,将该元素复制或移动到栈中。
-
emplace(): 在栈顶直接构造并插入一个元素,避免了额外的拷贝或移动操作。
-
pop(): 移除栈顶元素。
-
swap(): 交换两个
stack
对象的内容。
需要注意的是push()和emplace(),
push
和emplace
在插入内置类型情况下的效果是相同的。emplace
的优势在于,当你有一个更复杂的对象(如自定义类)时,可以直接在栈中使用构造函数参数创建对象,避免了临时对象的创建。(简而言之:当有自定义类型再考虑使用emplace)
上面介绍的已经是stack的绝大多少使用内容了,剩下的一些冷门一点的函数,这里不作介绍,大家感兴趣可以去参考网站上查看了解详细用法。
2.2queue
什么是queue(队列):
队列(Queue) 是一种数据结构,用于存储和管理数据并遵循先进先出(FIFO, First In, First Out)的原则。这意味着最早插入的元素会被最先移除,类似于排队等候的情况。
结合图形认识队列:
2.2.1queue类接口函数
queue的接口函数,和stack是一模一样的!!所以用法也是一摸一样。
不一样的是数据管理规则,栈是后进先出,队列是先进先出。
同样的类模板定义方式:
(默认传入的容器也是deque)
template <class T, class Container = std::deque<T>>
class queue {
... ...
};
接口函数:
empty()
: 返回队列是否为空。size()
: 返回队列中的元素数量。front()
: 返回队列头部元素的引用。back()
: 返回队列尾部元素的引用。push(const value_type& value)
: 在队列尾部插入一个元素。emplace(Args&&... args)
: 在队列尾部原地构造元素。pop()
: 移除队列头部的元素。swap(queue& other)
: 交换两个队列的内容。
函数命名和stack类中大部分一样的,不一样的是:
在队列中查看第一个元素:front()
栈中 查看栈顶元素:top()
但是,使用queue时,有一点需要注意:(非常重要)
queue
适配器不支持 vector
作为底层容器!!
queue
适配器不支持 vector
作为底层容器 !!
queue
适配器不支持 vector
作为底层容器 !!
为什么
queue
适配器不支持vector
作为底层容器 呢?
vector
不支持高效的头部删除操作,因为vector
中的元素在内存中是连续存储的。删除头部元素意味着需要移动整个数组中的元素,所有后面的数据都要向前挪。开发C++STL的前辈们可能觉得效率太低,所以直接不允许了(强行用会报错)。
2.2.2使用示例
#include <iostream>
#include <queue> // 包含 std::queue
int main() {
// 创建一个空的整数队列(使用默认容器deque)
std::queue<int> myQueue;
// 向队列中添加元素
myQueue.push(10);
myQueue.push(20);
myQueue.push(30);
// 打印队列的大小
std::cout << "size: " << myQueue.size() << std::endl;
// 访问队列头部的元素
std::cout << "myQueue.front(): " << myQueue.front() << std::endl;
// 访问队列尾部的元素
std::cout << "myQueue.back(): " << myQueue.back() << std::endl;
// 从队列中移除头部元素
myQueue.pop();
// 再次访问队列头部的元素
std::cout << "myQueue.front(): " << myQueue.front() << std::endl;
// 打印剩下的所有元素并清空队列
std::cout << "myQueue: ";
while (!myQueue.empty())
{
std::cout << myQueue.front() << " ";
myQueue.pop();
}
std::cout << std::endl;
return 0;
}
运行结果:
size: 3
myQueue.front(): 10
myQueue.back(): 30
myQueue.front(): 20
myQueue: 20 30
三、容器:deque
前文一直提到的deque容器,到底是个什么呢?
为什么stack和queue的默认容器都使用她?
详细的参考网站:cplusplus.com/reference/deque/deque/
我们来揭开她神秘的面纱:
deque
(双端队列,double-ended queue)是一种序列容器,它允许在容器的两端进行高效的插入和删除操作。
deque可以看作是vector和list的结合体为什么呢?用图形具体表示:
如图形所呈现,
depue的特点
deque
的内存分配是分段的,不像vector
那样需要一个连续的大块内存空间。它由多个固定大小的小块(通常称为缓冲区或块)组成,这些块通过中控数组来管理。deque
使用中控数组管理这些块。这个指针数组通常被称为map
,map
中的每个元素都是一个指向块的指针。通过这个map
,可以快速定位并访问任意块中的元素。- 当在
deque
的头部或尾部插入元素时,如果当前块已满,deque
会分配一个新的块,并将其指针添加到map
中。如果map
本身的容量不足,则会重新分配一个更大的map
并迁移现有的块指针。 - 块的大小通常是固定的,但具体大小取决于实现和平台
- 两端操作
为什么stack和queue的默认容器都使用她呢?
因为depue的两端操作:
- 当在头部插入元素时,如果前面的块有空间,直接插入即可;如果前面的块已满,则会分配一个新的块并将其插入
map
的前端。- 在尾部插入元素时,情况类似。如果当前尾部块有空间,则直接插入;如果没有空间,则会分配新的块。
这种设计允许它在两端进行高效的插入和删除操作,同时仍然随机访问能力。
缺点:
deque
支持常数时间的随机访问,但由于需要通过map
和块的组合来访问具体元素,随机访问的效率不如vector
。在deque
中,随机访问涉及多个指针跳转,这在某些情况下会导致比vector
更高的开销。deque
使用分段数组来存储元素,因此它的内存开销比vector
更大deque
使用分段数组来存储元素,当它修改/插入/删除块中数据时,需要很复杂的挪动数据,效率很低
总结:头插头删尾插尾删非常灵活高效,但中间数据部分管理效率很低,因此非常适合做stack和queue的默认容器,日常用vector和list效率更高,更好用。
deque常用接口
1.构造函数
explicit deque (const allocator_type& alloc = allocator_type());
explicit deque (size_type n, const value_type& val = value_type(),
const allocator_type& alloc = allocator_type());
template <class InputIterator>
deque (InputIterator first, InputIterator last,
const allocator_type& alloc = allocator_type());
deque (const deque& x);
std::deque<T> dq;
:默认构造函数,创建一个空的deque
。std::deque<T> dq(n);
:创建一个包含n
个元素的deque
。std::deque<T> dq(n, value);
:创建一个包含n
个元素的deque
,每个元素的值为value
。std::deque<T> dq({value1, value2, ...});
:使用初始化列表构造deque
。std::deque<T> dq(first,last);:
使用迭代器区间初始化。
2.元素访问
dq[i]
:访问第i
个元素(支持随机访问)。dq.at(i)
:访问第i
个元素,带边界检查。dq.front()
:访问deque
的第一个元素。dq.back()
:访问deque
的最后一个元素。
3.修改器元素访问
void push_back(const T& value);
void push_back(T&& value); // 针对右值引用的重载
void push_front(const T& value);
void push_front(T&& value); // 针对右值引用的重载
void pop_back();
void pop_front();
iterator insert(iterator pos, const T& value);
iterator insert(iterator pos, T&& value); // 针对右值引用的重载
iterator erase(iterator pos);
void clear();
dq.push_back(value)
:在deque
的尾部插入元素。dq.push_front(value)
:在deque
的头部插入元素。dq.pop_back()
:移除deque
尾部的元素。dq.pop_front()
:移除deque
头部的元素。dq.insert(pos, value)
:在deque
的指定位置pos
插入元素。dq.erase(pos)
:移除deque
指定位置pos
的元素。dq.clear()
:清空deque
中的所有元素。
4.容量
dq.size()
:返回deque
中元素的数量。dq.empty()
:判断deque
是否为空。
5.其他操作
dq.swap(dq2)
:交换两个deque
的内容。dq.assign(n, value)
:将deque
赋值为n
个value
。
使用示例
#include <iostream>
#include <deque>
int main() {
// 创建一个空的 deque
std::deque<int> dq;
// 在 deque 的尾部插入元素
dq.push_back(10);
dq.push_back(20);
dq.push_back(30);
// 在 deque 的头部插入元素
dq.push_front(5);
dq.push_front(2);
// 访问元素
std::cout << "第一个元素: " << dq.front() << std::endl; // 2
std::cout << "最后一个元素: " << dq.back() << std::endl; // 30
// 使用下标访问元素
std::cout << "第二个元素: " << dq[1] << std::endl; // 5
// 在指定位置插入元素
dq.insert(dq.begin() + 2, 7);
// 打印所有元素
std::cout << "dq : ";
for (int value : dq)
{
std::cout << value << " ";
}
std::cout << std::endl; // 输出: 2 5 7 10 20 30
// 移除头部和尾部的元素
dq.pop_front();
dq.pop_back();
// 再次打印
std::cout << "dq : ";
for (int value : dq) {
std::cout << value << " ";
}
std::cout << std::endl; // 输出: 5 7 10 20
// 清空 deque
dq.clear();
// 检查 deque 是否为空
if (dq.empty())
{
std::cout << "deque 已清空。" << std::endl; // 输出: deque 已清空。
}
return 0;
}
运行结果:
第一个元素: 2
最后一个元素: 30
第二个元素: 5
dq : 2 5 7 10 20 30
dq : 5 7 10 20
deque 已清空。
使用方法也非常的简单,和vector的使用很相像。