deque容器
deque 允许常数时间内对头端或尾端进行元素的插入或移除操作。,因为它是动态地以分段连续空间组合而成随时可以增加一块新的空间并拼接起来。
1. 中控器
#ifndef __STL_NON_TYPE_TMPL_PARAM_BUG
template <class T, class Ref, class Ptr, size_t BufSiz>
class deque {
public:
typedef T value_type;
typedef value_type* pointer;
...
protected:
typedef pointer** map_pointer;
map_pointer map; // 指向 map,map 是连续空间,其内的每个元素都是一个指针。
size_type map_size;
...
};
其示例图如下:deque 的结构设计中,map 和 node-buffer 的关系如下:
控制器map初始大小为8,用于存储缓冲区,当空间不够存储新的缓冲区时,就需要扩充map的空间,扩充方式与vector的扩充方式一致,以下是实现源码:
size_type _Newsize = 0 < this->_Mapsize ? this->_Mapsize : 1;
while (_Newsize - this->_Mapsize < _Count || _Newsize < _DEQUEMAPSIZ) // _DEQUEMAPSIZ = 8
{ // scale _Newsize to 2^N >= _Mapsize + _Count
if (max_size() / _DEQUESIZ - _Newsize < _Newsize)
_Xlen(); // result too long
_Newsize *= 2;
}
每次扩容之后,原来的缓冲区,从map下标 oldsize/2 开始存放,上下预留相同的空行,方便支持deque首尾元素的添加。
2. 迭代器
deque 是分段连续空间,维持其“整体连续”假象的任务,就靠它的迭代器来实现,也就是 operator++ 和 operator-- 两个运算子上面。
既然是分段连续,迭代器应该能指出当前的连续空间在哪里;其次,第二点因为缓冲区有边界,迭代器还应该要能判断,当前是否处于所在缓冲区的边缘,如果是,一旦前进或后退,就必须跳转到下一个或上一个缓冲区;
template <class T, class Ref, class Ptr, size_t BufSiz>
struct __deque_iterator {
// 迭代器定义
typedef __deque_iterator<T, T&, T*, BufSiz> iterator;
typedef __deque_iterator<T, const T&, const T*, BufSiz> const_iterator;
static size_t buffer_size() { return __deque_buf_size(BufSiz, sizeof(T)); }
// deque是random_access_iterator_tag类型
typedef random_access_iterator_tag iterator_category;
// 基本类型的定义, 满足traits编程
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
// node
typedef T** map_pointer;
map_pointer node;
typedef __deque_iterator self;
...
};
deque 的每一个缓冲区由设计了三个迭代器。
struct __deque_iterator {
...
typedef T value_type;
T* cur; // 此迭代器指向缓冲区现行元素
T* first; // 此迭代器指向缓冲区的头
T* last; // 此迭代器指向缓冲区的尾
typedef T** map_pointer;
map_pointer node; // 指向管控中心
...
};
inline size_t __deque_buf_size(size_t n, size_t sz) {
return n != 0 ? n : (sz < 512 ? size_t(512 / sz): size_t(1));
}
// 如果 n 不为0,则返回 n,表示缓冲区大小由用户自定义
// 如果 n == 0,表示 缓冲区大小默认值
// 如果 sz = (元素大小 sizeof(value_type)) 小于 512 则返回 512/sz
// 如果 sz 不小于 512 则返回 1
假设我们现在构造了一个 int 类型的 deque,设置缓冲区大小等于 32,这样一来,每个缓冲区可以容纳 32/sizeof(int) = 8(64位系统)个元素。经过一番操作之后,deque 现在有 20 个元素了,那么成员函数 begin() 和 end() 返回的两个迭代器应该是怎样的呢?如下图所示:
20 个元素需要 20/(sizeof(int)) = 5(图中只展示3个)个缓冲区。所以 map 运用了三个节点。迭代器 start 内的 cur 指针指向缓冲区的第一个元素,迭代器 finish 内的 cur 指针指向缓冲区的最后一个元素(的下一个位置)。
迭代器的操作:
set_node 跳出一个缓冲区。
void set_node(map_pointer new_node) {
node = new_node;
first = *new_node;
last = first + difference_type(buffer_size());
}
操作符 - 两对象之间的相减,a - b获得从b到a的长度。
difference_type operator-(const self& x) const {
return difference_type(buffer_size()) * (node - x.node - 1) + (cur - first) + (x.last - x.cur);
}
// 其中 (node - x.node - 1) 计算两根迭代器之间完整buffer的长度
// (cur - first) 计算末尾buffer的元素量
// (x.last - x.cur) 计算起始buffer的元素量
operator++ 操作代表是需要切换到下一个元素,这里需要先切换再判断是否已经到达缓冲区的末尾。
self& operator++() {
++cur; //切换至下一个元素
if (cur == last) { //如果已经到达所在缓冲区的末尾
set_node(node+1); //切换下一个节点
cur = first;
}
return *this;
}
operator-- 操作代表切换到上一个元素所在的位置,需要先判断是否到达缓冲区的头部,再后退。
self& operator--() {
if (cur == first) { //如果已经到达所在缓冲区的头部
set_node(node - 1); //切换前一个节点的最后一个元素
cur = last;
}
--cur; //切换前一个元素
return *this;
}
操作符 [ ]
reference operator[](size_type n){
return start[difference_type(n)];
}
操作符 *
reference operator*() const{
return *cur;
}
操作符 ->
pointer operator->() const{
return &(operator*());
}
3. deque数据结构
deque除了维护一个指向map的指针外,也维护start,finish两个迭代器,分别指向第一个缓冲区的第一个元素和最后缓冲区的最后一个元素(的下一个位置)。此外,它也必须记住目前的map大小。
#ifndef __STL_NON_TYPE_TMPL_PARAM_BUG
template <class T, class Ref, class Ptr, size_t BufSiz>
class deque {
public:
typedef T value_type;
typedef value_type* pointer;
typedef size_t size_type;
...
public:
typedef _deque_iterator<T, T&, T*, BufSiz> iterator;
protected:
typedef pointer** map_pointer;
protected:
iterator start;
iterator finish;
map_pointer map; // 指向 map,map 是连续空间,其内的每个元素都是一个指针。
size_type map_size;
...
};
4. 构造和析构
create_map_and_nodes
template <class T, class Alloc = alloc, size_t BufSiz = 0>
class deque {
...
public: // Basic types
deque() : start(), finish(), map(0), map_size(0){
create_map_and_nodes(0);
} // 默认构造函数
deque(const deque& x) : start(), finish(), map(0), map_size(0) {
create_map_and_nodes(x.size());
__STL_TRY {
uninitialized_copy(x.begin(), x.end(), start);
}
__STL_UNWIND(destroy_map_and_nodes());
}
// 接受 n:初始化大小, value:初始化的值
deque(size_type n, const value_type& value) : start(), finish(), map(0), map_size(0) {
fill_initialize(n, value);
}
deque(int n, const value_type& value) : start(), finish(), map(0), map_size(0) {
fill_initialize(n, value);
}
deque(long n, const value_type& value) : start(), finish(), map(0), map_size(0){
fill_initialize(n, value);
}
...
};
deque 中控器的配置函数:
void deque<T,Alloc,BufSize>::create_map_and_nodes(size_type_num_elements) {
//需要节点数= (每个元素/每个缓冲区可容纳的元素个数+1)
//如果刚好整除,多配一个节点
size_type num_nodes = num_elements / buffer_size() + 1;
//一个 map 要管理几个节点,最少 8 个,最多是需要节点数+2
map_size = max(initial_map_size(), num_nodes + 2);
map = map_allocator::allocate(map_size);
// 计算出数组的头前面留出来的位置保存并在nstart.
map_pointer nstart = map + (map_size - num_nodes) / 2;
map_pointer nfinish = nstart + num_nodes - 1;
map_pointer cur;//指向所拥有的节点的最中央位置
...
}
注意:这样带来的好处是可以使得头尾两边扩充的可能性和一样大,换句话来说,因为 deque 是头尾插入都是 O(1), 所以 deque 在头和尾都留有空间方便头尾插入。
// 如果 map 尾端的节点备用空间不足,符合条件就配置一个新的map(配置更大的,拷贝原来的,释放原来的)
void reserve_map_at_back (size_type nodes_to_add = 1) {
if (nodes_to_add + 1 > map_size - (finish.node - map))
reallocate_map(nodes_to_add, false);
}
// 如果 map 前端的节点备用空间不足,符合条件就配置一个新的map(配置更大的,拷贝原来的,释放原来的)
void reserve_map_at_front (size_type nodes_to_add = 1) {
if (nodes_to_add > start.node - map)
reallocate_map(nodes_to_add, true);
}
5. 插入和删除操作
5.1 向两端插入
void push_front(const value_type& __x)
{
//头部buffer空间足够时,直接从后往前插入
if (this->_M_impl._M_start._M_cur != this->_M_impl._M_start._M_first)
{
_Alloc_traits::construct(this->_M_impl, this->_M_impl._M_start._M_cur - 1, __x);
--this->_M_impl._M_start._M_cur;
}
else
_M_push_front_aux(__x);
}
void push_back(const value_type& t) {
if (finish.cur != finish.last - 1) {
construct(finish.cur, t);
++finish.cur;
}
else
push_back_aux(t);
}
5.2 中间插入
从中间插入需要根据迭代器位置进行插入,调用insert函数,一个insert源代码实现如下:
template <typename _Tp, typename _Alloc>
typename deque<_Tp, _Alloc>::iterator
deque<_Tp, _Alloc>::
#if __cplusplus >= 201103L
insert(const_iterator __position, const value_type& __x)
#else
insert(iterator __position, const value_type& __x)
#endif
{
// 这里迭代器是头端当前迭代器就直接变为从头端插入了
if (__position._M_cur == this->_M_impl._M_start._M_cur)
{
push_front(__x);
return this->_M_impl._M_start;
}
// 这里迭代器是尾端当前迭代器就直接变为从尾端插入了
else if (__position._M_cur == this->_M_impl._M_finish._M_cur)
{
push_back(__x);
iterator __tmp = this->_M_impl._M_finish;
--__tmp;
return __tmp;
}
// 这里才是正常从中间插入
else
return _M_insert_aux(__position._M_const_cast(), __x);
}
根据待插入位置来决定是移动前半部分还是后半部分,而是否需要扩充容器大小还是由头端插入和尾端插入完成的,这里可以看出,中间插入的时间复杂度为O(n)。
5.3 从两端删除
void pop_back() _GLIBCXX_NOEXCEPT
{
__glibcxx_requires_nonempty();
if (this->_M_impl._M_finish._M_cur != this->_M_impl._M_finish._M_first)
{
--this->_M_impl._M_finish._M_cur;
_Alloc_traits::destroy(this->_M_impl,
this->_M_impl._M_finish._M_cur);
}
else
_M_pop_back_aux();
}
如果当前位置不等于开始位置,则直接把当前位置向前移动一位,并把新的当前位置的元素销毁即可,也就是说尾端迭代器所指向的当前位置其实都是已经被删除了的数据,如果已经等于开始位置,则说明要换buffer了,此时就需要调用_M_pop_back_aux
template <typename _Tp, typename _Alloc>
void deque<_Tp, _Alloc>::_M_pop_back_aux()
{
_M_deallocate_node(this->_M_impl._M_finish._M_first);
this->_M_impl._M_finish._M_set_node(this->_M_impl._M_finish._M_node - 1);
this->_M_impl._M_finish._M_cur = this->_M_impl._M_finish._M_last - 1;
_Alloc_traits::destroy(_M_get_Tp_allocator(), this->_M_impl._M_finish._M_cur);
}
直接释放了当前尾端迭代器所在的buffer,然后先计算出来新的当前位置,最后才进行删除动作,根据该逻辑,尾端删除时间复杂度为O(1)。头端删除与尾端删除大同小异。
5.4 从中间删除
从中间删除会调用erase函数,deque容器有诸多erase函数的重载,我们选取其中一个进行解析,如下:
iterator
#if __cplusplus >= 201103L
erase(const_iterator __first, const_iterator __last)
#else
erase(iterator __first, iterator __last)
#endif
{ return _M_erase(__first._M_const_cast(), __last._M_const_cast()); }
_M_erase
template <typename _Tp, typename _Alloc>
typename deque<_Tp, _Alloc>::
iterator deque<_Tp, _Alloc>::_M_erase(iterator __first, iterator __last)
{
//如果开始位置等于结束位置,就不用删除了
if (__first == __last)
return __first;
//如果开始位置等于容器的开始位置,结束位置等于容器的结束位置,那么直接整个容器清空即可
else if (__first == begin() && __last == end())
{
clear();
return end();
}
else
{
const difference_type __n = __last - __first;
const difference_type __elems_before = __first - begin();
//与从中间插入逻辑类似,如果待插入数据段前面的元素少于后面的元素数量,则从头端进行处理,否则从尾端处理
if (static_cast<size_type>(__elems_before) <= (size() - __n) / 2)
{
//如果待删除数据段开始位置不等于容器开始位置,那么先把头端遗留数据向后覆盖
if (__first != begin())
_GLIBCXX_MOVE_BACKWARD3(begin(), __first, __last);
//删除多余元素
_M_erase_at_begin(begin() + __n);
}
else
{
//与if类似,这里不多说了
if (__last != end())
_GLIBCXX_MOVE3(__last, end(), __first);
_M_erase_at_end(end() - __n);
}
return begin() + __elems_before;
}
}
}
}
从中间删除,其实是先用头端或者尾端数据把要删除的数据覆盖掉,然后再从头端和尾端删除掉多余的数据,在这个过程中,如果待删除数据段有跨buffer,那么这个buffer也会被销毁。从中间删除元素的时间复杂度是O(n)。
6. deque成员函数
函数功能 | |
---|---|
begin() | 返回指向容器中第一个元素的迭代器。 |
end() | 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。 |
rbegin() | 返回指向最后一个元素的迭代器。 |
rend() | 返回指向第一个元素所在位置前一个位置的迭代器。 |
cbegin() | 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
cend() | 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
crbegin() | 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
crend() | 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
size() | 返回实际元素个数。 |
max_size() | 返回容器所能容纳元素个数的最大值。这通常是一个很大的值,一般是 232-1,我们很少会用到这个函数。 |
resize() | 改变实际元素的个数。 |
empty() | 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。 |
shrink _to_fit() | 将内存减少到等于当前元素实际所使用的大小。 |
at() | 使用经过边界检查的索引访问元素。 |
front() | 返回第一个元素的引用。 |
back() | 返回最后一个元素的引用。 |
assign() | 用新元素替换原有内容。 |
push_back() | 在序列的尾部添加一个元素。 |
push_front() | 在序列的头部添加一个元素。 |
pop_back() | 移除容器尾部的元素。 |
pop_front() | 移除容器头部的元素。 |
insert() | 在指定的位置插入一个或多个元素。 |
erase() | 移除一个元素或一段元素。 |
clear() | 移出所有的元素,容器大小变为 0。 |
swap() | 交换两个容器的所有元素。 |
emplace() | 在指定的位置直接生成一个元素。 |
emplace_front() | 在容器头部生成一个元素。和 push_front() 的区别是,该函数直接在容器头部构造元素,省去了复制移动元素的过程。 |
emplace_back() |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!