🚀 作者简介:一名在后端领域学习,并渴望能够学有所成的追梦人。
🚁 个人主页:不 良
🔥 系列专栏:🛸C++ 🛹Linux
📕 学习格言:博观而约取,厚积而薄发
🌹 欢迎进来的小伙伴,如果小伙伴们在学习的过程中,发现有需要纠正的地方,烦请指正,希望能够与诸君一同成长! 🌹
文章目录
- 认识deque
- stack简介
- stack常用接口
- stack模拟实现
- queue简介
- queue常用接口
- queue模拟实现
认识deque
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。
deque是双端队列,对标的是vector加list的合体。
template < class T, class Alloc = allocator<T> > class deque;
vector的优缺点:
优点:支持下标随机访问;
缺点:头部或者中部插入删除效率低下;扩容
list的优缺点:
优点:任意位置插入删除效率高效;
缺点:不能支持下标随机访问
deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组。
deque容器是开一个一个的小数组,当数组满了之后再开一个小数组;将这些小数组管理起来的数组是指针数组(中控(指针数组)),最开始使用的指针是中控指针数组中间位置的指针,当进行头插、尾插的时候,就可以直接使用前一个、后一个指针指向新开辟的空间。
中控满了之后就扩容,而且deque扩容给代价低,一个小数组对应一个中控数组中的指针,扩容代价低是因为只需要拷贝指针数组中的指针,然后再将原来的空间释放。
头插插入数组尾部,尾插在数组头部:
deque支持下标的随机访问,但是效率没有vector高。
deque的下标访问(每个数组大小固定): 假设每个小数组的容量为 10 ,我们想要找到下标为 25 的元素,用下标减去第一个数组内元素的个数,再除以每个小数组的容量就能找到其所在哪一个小数组。比如上图中就用(25 - 3) / 10 。找到对应元素存在于第 2 个数组后,再用 (25 - 3) % 10 就可以知道对应元素是在该小数组中的第几个。
deque容器中间插入删除时候很难搞,可以用size和capacity记录每个数组,可以每个数组不一样大,但是此时随机访问就麻烦了。
与vector比较,deque的优势是:头部插入和删除时,不需要移动元素,效率特别高,而且在扩容时,也不需要移动大量的元素,因此其效率是比vector高的。
与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
所以deque优点有:
- 相比vector,deque扩容代价低;
- 头插头删、尾插尾删效率高;
- 支持下标随机访问
缺点:
1.中间插入删除后很难搞(当每个数组不一样大时,中间插入删除的效率会变高但是随机访问的效率变低;当每个数组大小固定时,牺牲中间插入删除的效率,随机访问的效率就变高了);
2.没有vector和list优点极致
但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。
deque适用于什么情况?
如果头插头删多,尾插尾删多可以使用deque容器,所以很适合用作栈和队列的默认底层容器。所以默认作为栈和队列的底层适配容器。
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的优点,而完美的避开了其缺陷
stack简介
stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
- empty:判空操作
- back:获取尾部元素操作
- push_back:尾部插入元素操作
- pop_back:尾部删除元素操作
标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque。
stack常用接口
函数说明 | 接口说明 |
---|---|
stack() | 构造空的栈 |
empty() | 检测stack是否为空 |
size() | 返回stack中元素的个数 |
top() | 返回栈顶元素的引用 |
push() | 将元素val压入stack中 |
pop() | 将stack中尾部的元素弹出 |
库中stack定义方式接口:
template <class T, class Container = deque<T> > class stack;
缺省值为deque容器,说明底层容器为deque,我们也可以根据情况定义,所以定义方式有以下两种:
1.使用默认的适配器定义栈。
stack<int> st;//定义一个存储int类型的栈
2.:使用特定的适配器定义栈。
stack<int, vector<int>> st;//定义一个用vector<int>作为底层容器存储int类型的栈
stack<int, list<int>> st;//定义一个用list<int>作为底层容器存储int类型的栈
使用:
#include <iostream>
#include <stack>
using namespace std;
int main()
{
stack<int> st;//构造
//判断是不是空
cout << st.empty() << endl;//0
//插入元素
for (int i = 0; i < 4; i++)
{
st.push(i);
}
//判断是不是空
cout << st.empty() << endl;//1
//打印栈中元素个数
cout << st.size() << endl;
//打印,stack不支持迭代器
for (int i = 0; i < 4; i++)
{
cout << st.top() << " ";//打印栈顶元素
st.pop();//删除
}
//输出3 2 1 0
}
stack没有迭代器,所以遍历的时候不能使用范围for。
stack模拟实现
我们用适配器模式/配接器模式,本质是转换也就是把已有的东西进行转换,就好比我们的手机充电器并不能直接使用220v电压,所以提供了一个转换器,将电压转换为适合我们使用的。
设计模式就是指把常见的设计方法进行总结,适配器也是一种设计模式。
我们用已有的容器封装:可以这样定义类模板template<class T,class Container>
,Container就是符合我们要求的一个容器。
stack模拟实现主要依赖底层容器中的函数,所以stack模拟实现比较简单,将类模板中的第二个参数默认值为deque容器,目的是让deque作为默认底层容器。
namespace Niu {
//给类模板中的第二个参数默认值为deque容器
template<class T,class Container = deque<T>>
class stack {
public:
//实现push
void push(const T& val)
{
//调用容器的push_back函数
_con.push_back(val);
}
//实现pop
void pop()
{
//调用容器的pop_back函数
_con.pop_back();
}
const T& top()
{
//调用容器的back函数返回容器最后一个数据
return _con.back();
}
size_t size()
{
//调用容器的size函数
return _con.size();
}
bool empty()
{
//调用容器的empty函数
return _con.empty();
}
private:
Container _con;
};
}
queue简介
队列是一种容器适配器,专门用于在FIFO上下文(先进先出)的环境中操作,其中从容器一端插入元素,另一端提取元素。
队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
- empty:检测队列是否为空
- size:返回队列中有效元素的个数
- front:返回队头元素的引用
- back:返回队尾元素的引用
- push_back:在队列尾部入队列
- pop_front:在队列头部出队列
标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。
queue常用接口
函数声明 | 接口说明 |
---|---|
queue() | 构造空的队列 |
empty() | 检测队列是否为空,是返回true,否则返回false |
size() | 返回队列中有效元素的个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素val入队列 |
pop() | 将队头元素出队列 |
库中queue定义方式接口:
template <class T, class Container = deque<T> > class queue;
缺省值为deque容器,说明底层容器为deque,我们也可以根据情况定义,所以定义方式有以下两种:
1.使用默认的适配器定义栈。
queue<int> st;//定义一个存储int类型的栈
2.:使用特定的适配器定义栈。
注意queue中底层容器仅可使用deque和list容器,其他容器没有符合的对应操作。
queue<int, list<int>> st;//定义一个用list<int>作为底层容器存储int类型的栈
队列能不能用vector适配?不能,vector不适合头删,使用erase的话效率低。
使用:
#include <iostream>
#include <queue>
using namespace std;
int main()
{
queue<int> q;
//判断q队列中是不是为空
cout << q.empty() << endl;//0
// 插入元素
for (int i = 0; i < 5; i++)
{
q.push(i);
}
//获取队头元素
cout << q.front() << endl; //0
//获取队尾元素
cout << q.back() << endl; //4
//判断队列是否为空
cout << q.empty() << endl; //1
//队列中元素个数
cout << q.size() << endl; //5
//删除元素
for (int i = 0; i < 5; i++)
{
cout << q.front() << " ";
q.pop();//从队头开始删除
}
//输出0 1 2 3 4
return 0;
}
queue模拟实现
queue模拟实现和stack模拟实现差不多,都是通过调用容器的函数来完成对应的功能。
模板声明和定义分离,在不同的文件中,要单独加模板参数,即便加了之后也可能会出错。
模板声明和定义分离会有很多的问题,会产生链接错误。所以模板都定义在.h中就可以,不然会产生未知错误。
namespace Niu {
template<class T , class Container = deque<T>>
class queue {
public:
//判空
bool empty()
{
return _con.empty();
}
//在队尾插入元素
void push(const T& val)
{
_con.push_back(val);
}
//删除队头元素
void pop()
{
_con.pop_front();
}
//返回size大小
size_t size()
{
return _con.size();
}
//返回队头元素
T& front()
{
return _con.front();
}
//返回队头元素
const T& front() const
{
return _con.front();
}
//返回队尾元素
const T& back() const
{
return _con.back();
}
//返回队尾元素
T& back()
{
return _con.back();
}
private:
Container _con;
};
}