目录
- 1. deque 对象
- 1.1 内部表示
- 1.2 底层数据结构
- 1.3 分段连续
- 1.4 重新分配
- 2. deque 迭代器
本文测试环境 gcc 13.1
1. deque 对象
1.1 内部表示
deque 为我们提供了一个双端队列,即可以在队头进行 push、pop,可以在队尾进行 push、pop
deque 容器擅长的就是在头部和尾部添加或删除元素
vector 是一段连续的内存空间,当需要重新分配时,转移到了另一段连续的内存空间,而 deque 的实现比较复杂,deque 底层维护的是一段段连续的内存空间
首先看看 deque<T>
对象的内部表示
因为需要维护一段段的连续空间,里面保存了很多指针,其中的 iterator 是 deque 的迭代器
sizeof(deque<int>)
= 80,相比 vector 大很多
1.2 底层数据结构
下图是具体的模型
假设现在有一个 deque 对象保存了 1, 2, …, n, n+1, n+2 这样一个序列,那么它们可能就像如图所示的分布情况
左边是一个映射表,表中每一个指针指向一段连续的内存空间,通过这个表来将所有分段联系起来
右边是两段连续的内存空间,用来存放实际的元素,每一段空间所能容纳的元素个数是相同的。图中 0 ~ n 存放在一块,n + 1 存放在另一块,整个序列不是连续的,而是分段连续的
再看看 iterator ,node 指向映射表中的一个数据,来定位这段空间,first 和 last 分别指向这段空间的首地址和可用空间的末尾地址,iterator start
的 cur 指针指向第一个元素,而 iterator finish
的 cur 指针指向的是最后一个元素的下一个位置
1.3 分段连续
iterator 中 node 的作用就很关键了,想从一段空间跳到下一段空间,就需要将 node + 1 ,这样 node 指向了下一段内存空间,迭代器就可以访问下一段空间里的元素了,这就是 deque 实现的分段连续
底层 deque 申请了一段段的空间,通过这个 map 映射表,让用户感觉就是一段完整的连续空间
当头部或尾部要超出容量时,就需要分配新的一段空间,建立一个新的映射关系,在新的一段空间里构造新元素
1.在头部添加元素:
如图,现在想 push_front(0)
在头部添加一个元素,因为再添加已经超出空间,所以需要分配一个新的空间,添加之后情况如下
新添加的 0 在新空间的末尾
2.在末尾添加元素:
如图,现在想 push_back(n+1)
在末尾添加一个元素,但是现在的空间的元素个数已经用完了,需要分配一个新空间,添加之后情况如下
新添加的 n + 1 在新空间的首部
1.4 重新分配
vector 的重新分配需要将元素从旧空间转移至新空间
而 deque 的重新分配是指 map 映射表用完了,需要申请新空间容纳更多的映射关系,而实际存放元素的那些空间并不需要处理,只需要将旧映射表中的指针拷贝到新映射表就行了
2. deque 迭代器
deque 对象中已经使用了 start 和 finish 来定位首尾元素了,下面我们看看 deque 迭代器的具体实现
template<typename _Tp, typename _Ref, typename _Ptr>
struct _Deque_iterator
{
_Elt_pointer _M_cur; // T*
_Elt_pointer _M_first;
_Elt_pointer _M_last;
_Map_pointer _M_node; // T**
}
deque 迭代器实现为随机访问迭代器
cur 指针指向的就是具体的元素,通过 cur 就能访问元素了
_GLIBCXX_NODISCARD
reference
operator*() const _GLIBCXX_NOEXCEPT
{ return *_M_cur; }
_GLIBCXX_NODISCARD
pointer
operator->() const _GLIBCXX_NOEXCEPT
{ return _M_cur; }
++ – 也就是 cur 指针移动,这里就是要考虑 cur 到达 fisrt 或 last 需要跳转到另一段空间中,这就需要改变 node 了,同时修改 first 和 last ,如果 cur 需要跳转到前一段空间,那么 cur 应该指向这段空间的末尾也就是 last - 1,如果是跳转下一段空间,那么 cur 应指向这段空间的首地址也就是 first
_Self&
operator++() _GLIBCXX_NOEXCEPT
{
++_M_cur;
if (_M_cur == _M_last)
{
_M_set_node(_M_node + 1);
_M_cur = _M_first;
}
return *this;
}
_Self&
operator--() _GLIBCXX_NOEXCEPT
{
if (_M_cur == _M_first)
{
_M_set_node(_M_node - 1);
_M_cur = _M_last;
}
--_M_cur;
return *this;
}
跳转到另一个空间,修改 node
void
_M_set_node(_Map_pointer __new_node) _GLIBCXX_NOEXCEPT
{
_M_node = __new_node;
_M_first = *__new_node;
_M_last = _M_first + difference_type(_S_buffer_size());
}
这样就可以在分段的空间上遍历所有元素了