STL c++ 详解——stack与queue模拟实现与deque的介绍
stack的介绍和模拟实现
stack的介绍
函数说明 | 接口说明 | |||||
stack() | 构造空的栈 | |||||
empty() | 检测stack是否为空 | |||||
size() | 返回stack中元素的个数 | |||||
top() | 返回栈顶元素的引用 | |||||
push() | 将元素val压入stack中 | |||||
pop() | 将stack中尾部的元素弹 |
stack的模拟实现
#include<vector>
namespace bite
{
template<class T>
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:
std::vector<T> _c;
};
}
queue的介绍和模拟实现
queue的介绍
队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供
一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
函数声明 | 接口说明 | |||||
queue() | 构造空的队列 | |||||
empty() | 检测队列是否为空,是返回true,否则返回false | |||||
size() | 返回队列中有效元素的个数 | |||||
front() | 返回队头元素的引用 | |||||
back() | 返回队尾元素的引用 | |||||
push() | 在队尾将元素val入队列 | |||||
pop() | 将队头元素出队列 |
模拟实现
因为queue的接口中存在头删和尾插,因此使用vector来封装效率太低,故可以借助list来模拟实
现queue,具体如下:
#include <list>
namespace bite
{template<class T>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<T> _c;};
}
容器适配器
适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设
计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。
上面的queue和stack都是容器适配器
STL标准库中stack和queue的底层结构
虽然这两个也是可放元素的,但是没有划分到STL容器中,而是管他们叫容器适配器。他们可以接收其他容器的接口作为底层结构。
STL中stack和queue默认 使用deque,比如:
deque介绍
为什么stack和queue默认 使用deque,而不用其他容器。请看👇
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端
进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与
list比较,空间利用率比较高。这种特性使得 deque
在很多场景下比普通队列更具优势,比如需要频繁在两端操作数据的情况。
-
中控器(map):图中的
map
是deque
的中央控制器,本质上是一个指针数组。它存储着指向各个缓冲区的指针,用于管理和定位deque
中元素的存储位置。当deque
需要扩展或访问元素时,通过map
可以快速找到对应的缓冲区。 -
缓冲区:多个固定大小的连续内存块,用于实际存储
deque
的元素。每个缓冲区能容纳一定数量的元素,当一个缓冲区存满后,deque
会利用map
找到或分配新的缓冲区来继续存储元素。图中还提到当map
使用率满载时,需要重新分配更大空间作为map
,这涉及到内存管理和数据迁移操作,以保证deque
能持续正常工作。
deque
内容
deque
的迭代器是一个复杂的对象,它封装了对中控器和缓冲区的操作。迭代器包含以下几个重要成员:
-
cur
:指向当前缓冲区中正在访问的元素。 -
first
:指向当前缓冲区的起始位置。 -
last
:指向当前缓冲区的结束位置。 -
node
:指向中控器中当前缓冲区对应的指针。
通过这些成员,迭代器可以在不同的缓冲区之间移动,实现对 deque
中元素的遍历。当迭代器移动到当前缓冲区的边界时,它会通过 node
指针找到下一个或上一个缓冲区,并更新 cur
、first
和 last
指针。
插入和删除操作
-
两端插入和删除:在
deque
的两端插入或删除元素时,通常只需要在当前的缓冲区进行操作,或者在必要时分配或释放一个新的缓冲区。因此,在两端插入和删除元素的时间复杂度为 \(O(1)\)。 -
中间插入和删除:在
deque
的中间插入或删除元素时,需要移动部分元素。具体来说,需要将插入或删除位置之后的元素在缓冲区之间进行移动,以腾出或填充空间。因此,在中间插入或删除元素的时间复杂度为 \(O(n)\),但由于deque
的分段连续存储结构,移动的元素数量通常比vector
要少。
为什么选择deque作为stack和queue的底层默认容器
stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性
结构,都可以作为stack的底层容器,比如vector和list都可以;queue是先进先出的特殊线性数据
结构,只要具有push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如
list。但是STL中对stack和queue默认选择deque作为其底层容器,主要是因为:
- 1. stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进 行操作。
- 2. 在stack中元素增长时, deque比vector的效率高(扩容时不需要搬移大量数据);queue中的 元素增长时, deque不仅效率高,而且内存使用率高。
结合了deque的优点,而完美的避开了其缺陷。