使用stack,queue和deque需要包含头文件stack,queue和deque
目录
8.4 stack和queue选择deque作为底层容器的原因
一、stack的介绍
栈 【stack】是一种 特殊的数据结构,也是一种容器适配器,主要特点为:【先进后出】,主要操作有:入栈、出栈、查看栈顶元素、判断栈空等;栈在原则上是不允许进行中部或底部操作的,这样会破坏栈结构的完整性。

我们在stack的参考文档可以看见栈的定义,如下所示:

可以看到 栈【stack】是作为容器适配器被实现,容器适配器是对特定类封装作为其底层容器(容器适配器本文的后面会详细讲解,这里先了解一下它 和 栈 的关系)
可以看出,栈有两个模板参数
- 参数1:
T栈中的元素类型,同时也是底层容器中的元素类型 - 参数2:
Container实现栈时用到的底层容器,这里为缺省参数,缺省结构为 双端队列deque
也就是说,【stack】的底层容器可以是任何标准的容器类模板或者一些其特定的容器类,这些容器应该支持一下操作:
- empty :判空操作
- top :获取栈顶元素
- push_back :尾部插入元素操作
- pop_back :尾部删除元素操作
标准容器 vector、deque、list 均符合上面的这些要求,默认情况下,如果没有为 【stack】指定特定的顶层容器,默认情况下使用 deque (这一点会在本文,后下容器适配器,详细讲解)
二、stack的常用函数
2.1 构造函数
2.1.1 默认构造函数
#include<iostream>
using namespace std;
#include<stack>
int main() {
stack<int> st;
return 0;
}
2.1.2 拷贝构造函数
#include<iostream>
using namespace std;
#include<stack>
int main() {
stack<int> st1;
st1.push(1);
st1.push(2);
st1.push(3);
stack<int> st2(st1);
return 0;
}
2.1.3 使用其它的容器构造一个栈
#include<iostream>
using namespace std;
#include<stack>
#include<vector>
int main() {
stack<int, vector<int>> st;
return 0;
}
2.2 push--入栈
这个函数的作用是往栈里面入数据
#include<iostream>
using namespace std;
#include<stack>
int main() {
stack<int> st1;
st1.push(1);
st1.push(2);
st1.push(3);
st1.push(4);
return 0;
}
2.3 size--获取元素个数
这个函数的作用是求栈中的元素个数
#include<iostream>
using namespace std;
#include<stack>
int main() {
stack<int> st1;
st1.push(1);
st1.push(2);
st1.push(3);
cout << st1.size() << endl;
return 0;
}
2.4 top--取栈顶元素
这个函数的作用是取出栈顶元素
#include<iostream>
using namespace std;
#include<stack>
int main() {
stack<int> st1;
st1.push(1);
st1.push(2);
st1.push(3);
st1.push(4);
cout << st1.top()<< endl;
return 0;
}
2.5 empty--判空
这个函数的作用是判断一个栈是否为空,空就返回true,非空就返回false。
#include<iostream>
using namespace std;
#include<stack>
int main() {
stack<int> st1;
st1.push(1);
st1.push(2);
st1.push(3);
cout << st1.empty() << endl;
return 0;
}
2.6 pop--出栈
这个函数的作用是删除栈顶元素,注意这个和top函数的区别,top函数只是取出栈顶元素,不会删除,这个函数会删除栈顶元素。
#include<iostream>
using namespace std;
#include<stack>
int main() {
stack<int> st1;
st1.push(10);
st1.push(20);
st1.push(30);
st1.push(40);
st1.pop();//删除栈顶元素40
cout << st1.top() << endl; //取出栈顶元素30
return 0;
}
三、queue的介绍
队列 【queue】是一种特殊的数结构,同时也是一种 容器适配器,遵循先进先出FIFO原则,其中从容器一端插入元素,另一端提取元素。主要操作:入队、出队、判断队空、查看队头队尾元素等;队列在原则上 是不允许进行 中部 的操作,这样会破坏队列的完整性。

我们在queue文档介绍可以看见queue的定义,如下所示:

可以看到 队列【queue】也是作为容器适配器被实现,容器适配器是对特定类封装作为其底层容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层。元素从队尾入列,从队头出列。(容器适配器本文的后面会详细讲解,这里先了解一下它 和 队列 的关系)
可以看出,队列也有两个模板参数
- 参数1:
T栈中的元素类型,同时也是底层容器中的元素类型 - 参数2:
Container实现栈时用到的底层容器,这里为缺省参数,缺省结构为 双端队列deque
也就是说,【queue】的底层容器可以是任何标准的容器类模板或者一些其特定的容器类,这些容器应该支持一下操作:
- empty :检查队列是否为空
- size :返回队列中的有效元素个数
- front :返回队头元素的引用
- back :返回队尾元素的引用
- psuh_back :在队列尾部入队列
- pop_front :在队列头部出队列
四、queue的常用函数
4.1 构造函数
4.1.1 默认构造函数
#include<iostream>
using namespace std;
#include<queue>
int main() {
queue<int> q1; //底层结构默认是deque
return 0;
}
4.1.2 拷贝构造函数
#include<iostream>
using namespace std;
#include<queue>
int main() {
queue<int> q1;
q1.push(1);
q1.push(2);
q1.push(3);
q1.push(4);
queue<int> q2(q1);
return 0;
}
4.1.3 使用其它的容器构造队列
#include<iostream>
using namespace std;
#include<queue>
#include<list>
int main() {
queue<int,list<int>> q1; //底层结构是双向链表
return 0;
}
4.2 push--入队
#include<iostream>
using namespace std;
#include<queue>
int main() {
queue<int> q1;
q1.push(1);
q1.push(2);
q1.push(3);
q1.push(4);
return 0;
}
4.3 pop--出队
#include<iostream>
using namespace std;
#include<queue>
int main() {
queue<int> q1;
q1.push(1);
q1.push(2);
q1.push(3);
q1.push(4);
q1.pop();
return 0;
}
4.4 front--取队头元素
#include<iostream>
using namespace std;
#include<queue>
int main() {
queue<int> q1;
q1.push(1);
q1.push(2);
q1.push(3);
q1.push(4);
cout << q1.front() << endl;
return 0;
}
4.5 back--取队尾元素
#include<iostream>
using namespace std;
#include<queue>
int main() {
queue<int> q1;
q1.push(1);
q1.push(2);
q1.push(3);
q1.push(4);
cout << q1.back() << endl;
return 0;
}
4.6 empty--判空
#include<iostream>
using namespace std;
#include<queue>
int main() {
queue<int> q1;
q1.push(1);
q1.push(2);
q1.push(3);
q1.push(4);
cout << q1.empty() << endl;
return 0;
}
4.7 size--获取队列元素个数
#include<iostream>
using namespace std;
#include<queue>
int main() {
queue<int> q1;
q1.push(1);
q1.push(2);
q1.push(3);
q1.push(4);
cout << q1.size() << endl;
return 0;
}
五、容器适配器
5.1 什么是容器适配器
适配器是一种设计模式(设计模式是一套被反复使用的、多人知晓的、经过分类编目的、代码设计经验的总结),该设计模式是将一个类的接口转换成客户希望的另一个接口

其中,容器适配器 可修改底层为指定容器,如由 vector 构成的栈、由 list 构成的队列;
5.2 STL标准库中stack和queue的底层结构
虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和队列只是对其他容器的接口进行了包装,STL中stack和queue默认用deque,比如:


注意: 容器支持 迭代器,但是容器适配器不支持迭代器,因为栈和队列这种数据结构不能随便去遍历,不然会导致发生变化,不易维护
六、栈的模拟实现
6.1 栈底层结构的构建
我们需要像string一样,使用T* _a,size_t _top和size_t _capacity来模拟构建栈嘛?,如下所示:
#pragma once
#include<iostream>
using namespace std;
namespace zx {
template<class T>
class stack
{
private:
T* _a;
size_t _top;
size_t _capacity;
};
}
我们实现栈能不能使用其他的容器封装转换一下,就转化出对应的栈呢?
栈只需要实现在栈顶插入,在栈顶删除,只有这个容器在一端进行插入和删除,就可以使用这个容器来模拟实现栈。如vector,list。所以栈就不需要来原生的来实现,来封装一下vector和list就可以了。如下所示:
#pragma once
#include<iostream>
using namespace std;
namespace zx {
template<class T>
class stack
{
private:
vector<T> v;
};
}
但是这样写的话就写死了,栈的底层结构就只能是vector了,如果我们有时候需要使用链表类型的栈呢?所以我们应该把这个类型写成模板,可以随时切换,如下所示:
#pragma once
#include<iostream>
using namespace std;
namespace zx {
template<class T,class Container>
class stack
{
private:
Container _con;
};
}
使用Container来定义一个容器,这个Container是什么类型我们也不知道,传什么就是什么,传入vector就是vector实现的栈,传入list就是list实现的栈。对Container适配转换出stack。
6.2 栈的构造函数
我们需要写stack的构造函数嘛?
因为stack里面的数据是自定义类型,无论我们写不写它都会调用Container的构造函数,所以可以不用写构造函数。
6.3 其他成员函数
因为stack的底层结构是vector或者list,我们完全就可以使用vector和list的函数来实现stack的函数,如下所示:
#pragma once
#include<iostream>
using namespace std;
#include<vector>
#include<list>
namespace zx {
template<class T,class Container>
class stack
{
public:
//入栈
void push(const T& x)
{
_con.push_back(x);
}
//出栈
void pop()
{
_con.pop_back();
}
//取栈顶元素
const T& top() const
{
return _con.back();
}
size_t size() const
{
return _con.size();
}
bool empty() const
{
return _con, empty();
}
private:
Container _con;
};
}
6.4 测试
#include"Stack.h"
int main() {
zx::stack<int, vector<int>> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
while (!st.empty()) {
cout << st.top() << endl;
st.pop();
}
return 0;
}
运行结果如下:

我们在声明这个栈的时候也可以这样来声明:zx::stack<int, list<int>> st;使用list来声明栈,此时stack的底层结构就成为了双向链表了。
6.5 给Container设置缺省值
但是我们在使用库里面的stack的时候并不需要传入vector和list类型,这是为什么呢?
我们可以在设置模板参数的时候给Container设置一个缺省值,使用默认的容器,如下所示:
template<class T,class Container=vector<T>>
此时,我们创建一个stack对象就不需要传入容器了,默认是vector容器,如果我们想要使用链表作为stack的底层结构,就可以传入list容器。
库里面使用的是deque容器,这个结构是一个双端队列,下面再讲
6.6 代码
6.6.1 stack.h
#pragma once
#include<iostream>
using namespace std;
#include<vector>
#include<list>
namespace zx {
template<class T,class Container=vector<T>>
class stack
{
public:
//入栈
void push(const T& x)
{
_con.push_back(x);
}
//出栈
void pop()
{
_con.pop_back();
}
//取栈顶元素
const T& top() const
{
return _con.back();
}
size_t size() const
{
return _con.size();
}
bool empty() const
{
return _con.empty();
}
private:
Container _con;
};
}
6.6.2 test.cpp
#include"Stack.h"
int main() {
zx::stack<int, vector<int>> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
while (!st.empty()) {
cout << st.top() << endl;
st.pop();
}
return 0;
}
七、队列的模拟实现
我们写完栈的模拟实现之后,队列的模拟实现也是如此,如下所示:
#pragma once
#include<iostream>
using namespace std;
#include<vector>
#include<list>
namespace zx {
template<class T, class Container = list<T>>
class queue
{
public:
//在队尾入队
void push(const T& x)
{
_con.push_back(x);
}
//在队头出队
void pop()
{
_con.pop_front();
}
//取队尾元素
const T& back() const
{
return _con.back();
}
//取队头元素
const T& front() const
{
return _con.front();
}
size_t size() const
{
return _con.size();
}
bool empty() const
{
return _con.empty();
}
private:
Container _con;
};
}
我们在上面使用list作为默认的容器,这是因为vector不直接支持头删,间接支持头删,并且头删效率很低。因此使用list。
测试代码如下:
#include"Queue.h"
int main() {
zx::queue<int> q1;
q1.push(1);
q1.push(2);
q1.push(3);
q1.push(4);
while (!q1.empty()) {
cout << q1.front() << endl;
q1.pop();
}
return 0;
}
八、deque的介绍
在STL中stack和queue默认用deque作为Container,下面我们就来介绍一些deque。
8.1 deque的介绍
双端队列【deque】:是一种双开口 ”连续“ 空间的数据结构,双开口的含义:可以在头尾端进行插入和删除操作,且时间复杂度为:O(1) ,与【vector】比较,头插效率高,不需要移动元素;与【list】比较,空间利用率比较高

8.2 deque的底层结构
deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:

deque(双端队列)的底层结构通常由多个固定大小的缓冲区组成,每个缓冲区是一个连续的存储块。然后使用一个中控指针数组来记录每一个缓冲区的起始地址,这样就能记录每一个缓冲区。
deque的内部缓冲区以分块的形式存储元素。每个缓冲区有一个固定的大小,它通常是2的幂次方,例如512、1024等。缓冲区中的元素被存储在数组中,以保持元素的连续性。
双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:

当需要在deque的头部或尾部插入或删除元素时,只涉及到相关缓冲区的操作,而不会涉及其他缓冲区。这种设计使得deque的插入和删除操作时间复杂度为常数级别(O(1))。

8.3 deque的优缺点
【vector】的优缺点:
- 优点:适合尾插尾删,随机访问
- 缺点:不适合头部或者中部插入删除,效率低,需要挪动数据;扩容有一定性能消耗,还可能存在一定程度的空间浪费。
【list】的优缺点:
- 优点:在任意位置插入删除效率高;按需申请释放空间
- 缺点:不支持随机访问;CPU高速缓存命中率低
【deque】就结合了 【vector】和 【list】的优缺点而为之发明!!!
【deque】 与 【vector】相比较 的优势:头部插入和删除时,不需要搬移元素,效率特别高,而且效率特别高,而且在扩容时,也不需要移动大量元素,因此其效率是比 【vector】高。
【deque】 与 【list】相比较 的优势:其底层是连续空间,空间利用率较高,不需要存储额外字段
【deque】的致命缺陷:不适合遍历!!! 因为在遍历时,【deque】的迭代器要频繁的去检查其是否移动到某段空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑【vector】和 【list】。
8.4 stack和queue选择deque作为底层容器的原因
【stack】是一种,后进先出的特殊线性数据结构,因此只要具有 push_back() 和 pop_back() 操作的线性结构,都可以作为 【stack】的底层容器,比如 【vector】和 【list】都可以
【queue】是一种,先进先出的特殊线性数据结构,因此只要具有 push_back() 和 pop_back() 操作的线性结构,都可以作为 【queue】的底层容器,比如 【list】
注意:string 、vector 不直接支持头删,只是间接支持头删除,并且头删效率低,因此无法适配 queue
但是在 STL 中对 【stack】和 【queue】默认选择 【queue】作为其底层容器,原因为:
【stack】 和 【queue】不需要遍历(因此stack 和 queue 没有迭代器),只需要在固定的一端或者两端进行操作。
在【satck】中元素增长时,【deque】比 【vector】的效率更高(扩容时不需要移动大量元素)
在【queue】中元素增长时,【deque】不仅效率高,而且内存使用率高
总结来说:【deque】结合了所有 【stack】 和 【queue】所需要的优点,而完美的避开了其缺陷
C++栈队列与deque详解
3245

被折叠的 条评论
为什么被折叠?



