目录
3.4 deque的优缺点 —— 为什么使用deque作为stack和queue的默认适配容器
一. 什么是stack和deque
stack是数据结构中的栈,遵循后进先出规则,queue是数据结构中的队列,遵循先进先出规则。
二. stack和queue的使用方法
2.1 stack的常用接口
接口函数名 | 功能 |
---|---|
push | 数据压栈 |
pop | 数据出栈 |
top | 获取栈顶元素 |
size | 获取栈中元素个数 |
empty | 检查栈是否为空 |
int main()
{
stack<int> st; //创建存储int型数据的栈
st.push(1);
st.push(2);
st.push(3);
st.push(4); //压栈操作,依次压入1、2、3、4
cout << "is empty:" << st.empty() << endl; //检查栈是否为空
cout << "size of stack:" << st.size() << endl; //检查栈中数据个数
st.pop(); //栈顶数据出栈
cout << "top = " << st.top() << endl; //获取栈顶数据
return 0;
}
2.2 queue的常用接口
接口函数名 | 功能 |
---|---|
push | 队尾入数据 |
pop | 队头出数据 |
empty | 判断队列是否为空 |
size | 获取队列中数据个数 |
front | 获取队头数据 |
back | 获取队尾数据 |
int main()
{
queue<int> q; //创建存储int型数据的队列
q.push(1);
q.push(2);
q.push(3);
q.push(4); //队头入数据
cout << "is empty:" << q.empty() << endl; //检查队列是否为空
cout << "size of queue:" << q.size() << endl; //检查队列中数据个数
q.pop(); //队头数据出队列
cout << "front = " << q.front() << endl;
cout << "back = " << q.back() << endl; //获取队头数据和队尾数据
return 0;
}
三. stack和queue的底层实现原理
3.1 容器适配器
C++ STL中给出的stack和queue类的声明为:
- template <class T, class Container = deque<T>> class stack;
- template <class T, class Container = deque<T>> class queue;
由stack和queue的声明不难发现,它们本质上并不是容器,而是容器适配器,
stack和queue的底层都是调用名为deque(双端队列)的容器的接口来实现的。
所谓适配器,通俗讲就是一种设计模式,即:一套被反复使用的、为多数人所知晓的、经过分类编目的代码设计经验的总结,设计模式可以将一个类接口转换为用户希望的其它类接口。
3.2 deque(双端队列)的概念及抽象结构
deque是双端队列容器,它可以同时支持O(1)时间复杂度下的头部的插入删除和在尾部的插入和删除,同时,可以通过下标来访问任意位置处的元素。
- 对于vector,它可以支持O(1)时间复杂度下的尾插数据和尾删数据,但要实现头插和头删则需要O(N)的时间复杂度。
- 对于list,它可以实现在O(1)时间复杂度下任意位置的插入和删除数据,但不能支持通过下标来进行随机访问。
所以,在某种程度上,我们可以认为deque兼具了vector和list的一些优点。
3.3 deque的底层实现结构
-
虽然在deque的抽象结构图中,数据在deque中是连续存储的。但是,
deque的真是实现是在内存堆区申请一块块的小空间来存储数据的,
这一块块小空间维持了deque整体连续的表象。 - 在deque类被实例化时,首先开辟一块内存空间,向deque对象中尾插数据时,首先检查当前的小块空间是否已满,如果满,则新开辟一小块空间,在新开辟的空间中插入数据,如果不满,则直接在当前的小块空间中插入数据。
- 在执行头插数据操作时,先检查当前小块空间的前部是否还有空间,如果有,则直接在当前空间的前部插入数据,如果没有,则新开辟一块空间,在新开辟的空间的最后一个位置插入数据。
- 通过一个中控指针数组,来模拟拼接每一小块空间。中控指针数组中存储每一小块内存空间的地址,中控指针数组下标小的位置处记录的地址中存储的数据位于双端队列的前部,下标大的位置对应双端队列的后部。
3.4 deque的优缺点 —— 为什么使用deque作为stack和queue的默认适配容器
虽然deuqe兼具了vector和list的一些优点,但是,deuqe依然极少作为单独的数据结构容器来使用,而是仅仅作为stack和queue等数据结构的适配容器。
deque相对于vector的优点和缺点
-
优点:
- deque支持以O(1)的时间复杂度完成头插和头删数据操作,vector如果头插和头删数据操作需要移动后面的所有数据,时间复杂度为O(N),效率相对于deque较低。
- deque扩容时不需要复制数据,而且deque一次开辟一小块空间,空间利用率高。而vector在扩容时需用复制已有数据到新的内存空间,并且每次扩容得到新空间的容量一般是原来容量的1.5倍或2倍,vector扩容时复制数据需要一定的消耗且空间浪费相对于deque较多。
-
缺点:
- 不适合遍历和随机访问,因为deuqe在进行遍历时,迭代器需要频繁地去检查是否移动到了某一小段空间的边界位置,效率低下。
deque相对于list的优点和缺点
-
优点:
- deque底层空间连续,不需要频繁地申请和释放小块的内存空间,申请和释放空间的消耗相对于list较低。
- 由于deque底层空间连续,CPU高速缓存命中率高。
- 不需要每个存储每个数据时都存储两个指针来指向前一个和后一个数据的位置。
-
缺点:
- 无法在O(1)时间复杂度的下实现在中间某个位置插入和删除数据。
为什么使用deque作为stack和queue的底层适配容器
如果没有deuqe容器,那么,stack只能使用vector或list来实现,queue只能使用list来实现。
-
相对于vector实现stack:
deque的扩容代价低,扩容不用拷贝数据,空间浪费较少。 -
相对于list实现stack:
deque不用频繁申请和释放小块内存空间,CPU高速缓存命中率高,申请和释放内存空间的次数少。 -
相对于list实现queue:
deque不用频繁申请和释放小块内存空间,CPU高速缓存命中率高,申请和释放内存空间的次数少。
3.5 通过deque容器来模拟实现stack和queue
下表为deuqe所支持的常规操作,我们不难发现,deque支持stack和queue的全部操作,因此,只需要在stack和queue接口的实现中调用deque容器对应的接口,就可以完成对stack和deque的模拟实现。
deque的接口函数 | 实现的功能 |
---|---|
size | 获取数据个数 |
resize | 删除数据 或 扩容+初始化新增空间 |
empty | 判断是否为空 |
operator[] | 通过下标访问数据 |
push_back | 尾插数据 |
pop_back | 尾删数据 |
push_front | 头插数据 |
pop_front | 头删数据 |
insert | 在特定位置插入数据 |
erase | 删除特定位置的数据 |
clear | 清空数据 |
模式实现queue:
#include<cstdbool>
#include<deque>
namespace zhang
{
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();
}
//数据量获取函数
size_t size() const
{
return _con.size();
}
//判空函数
bool empty() const
{
return _con.empty();
}
//获取队头数据函数
const T& front() const
{
return _con.front();
}
T& front()
{
return _con.front();
}
//获取队尾数据函数
const T& back() const
{
return _con.back();
}
T& back()
{
return _con.back();
}
private:
Container _con;
};
}
模式实现stack:
#include<deque>
#include<cstdbool>
namespace zhang
{
template <class T, class Container = std::deque<T>>
class stack
{
public:
void push(const T& x) //压栈 -- 数据插入函数
{
_con.push_back(x);
}
void pop() //出栈 -- 数据删除函数
{
_con.pop_back();
}
bool empty() //判空函数
{
return _con.empty();
}
T& top() //栈顶数据获取函数
{
return _con.back();
}
const T& top() const
{
return _con.back();
}
size_t size() const //获取栈中数据个数
{
return _con.size();
}
private:
Container _con; //用于实现栈的容器
};
}