前言
deque是一个很好用的双端队列,在头部或尾部插入删除复杂度都为O(1) 。关于deque操作方面就不怎么提了,和 vector 差不多 。这篇文章主要记录下deque的底层结构,这才是它有趣的地方。
deque结构
包括三个部分:迭代器(iterator)、中控器(map)、缓冲区(实际储存地址)。它们的关系如下图所示。
中控器(map)
这个结构也叫map,但是和STL里的map不一样,对于它来说,它是一个二维指针,每个节点都是指向一块真正储存数据的地方(缓冲区)。在这里,map的节点是连续的,每个缓冲区也是连续的,但是缓冲区之间可能在隔离的地址上。
每当一个缓冲区存满数据,我们就用已有的未使用的map节点,指向一个新的缓冲区。若已经达到map的最大值,又要重新分配新的map内存,这点和vector很像。
看下它的代码结构,就比较清晰的分层,map是一个二维指针,每个指针又指向块缓冲区地址。
template<class T, class Alloc = alloc, size_t Bufsize = 0>
class deque{
public: //basic types
typedef T value_type;
typedef value_type* pointer;
...
protected: //internal typedefs
//pointer of pointer of T
typedef pointer* map_pointer;
protected: //Data members
map_pointer map;//中控器map
size_type map_size;
...
}
迭代器(iterator)
接下来说说很关键的一个部分:迭代器。
我们现在拥有中控器和缓冲区,现在想做到明确地知道访问着哪个节点,map的范围是多少,现在访问着缓冲区哪个数据等,就需要迭代器来提供帮助,因此想想如果是自己写,会给迭代器结构设置哪些成员呢 ?
下面是一种实现方式
template<class T, class Ref, class Ptr, size_t BufSiz>
struct _deque_iterator{ //没有继承 std::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));}
//没继承iterator,自行编写五个必要迭代器的响应类别
typedef random_access_iterator_tag iterator_category;//(1)
typedef T value_type;//(2)
typedef Ptr pointer; //(3)
typedef Ref reference; //(4)
typedef size_t size_type;
typedef ptrdiff_t difference_type;//(5)
typedef T** map_pointer;//指向map
typedef _deque_iterator self;
//保持与容器的联结
T* cur; //所指的缓冲区当前访问元素
T* first; //所指的缓冲区头
T* last; //所指的指向缓冲区尾
map_pointer node;//管理map
...
}
我们可以看到,决定缓冲区大小的函数 buffer_size(),调用 _deque_buf_size(), 后者是一个全局函数,有如下定义:
- n不为0,传回n,buffer_size由用户定义
- n 为0,buffer_size为自定义值
- 如果sizeof(value_type)小于512,传回 512/sizeof(value_type)
- 如果sizeof(value_type)不小于512,传回1
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));
}
总结一下,迭代器关键就是 cur、first、end、node这四个指针。
例子
下面看一个例子体会下deque数据分配的操作。
情景
假设产生一个 deque< int >,并令其缓冲区大小为32 ,于是每个缓冲区可以容纳 32/sizeof(int) ,也就是 8个元素。
然后经过一些操作之后,我们拥有了20个元素,此时调用begin()和end()就会返回如图所示的两个迭代器。20个元素 等于 8*2 +4,也就是要三个缓冲区。
> 然后更新所有值,并且填满后,在后面添加数据,情况如下
在前面增加数据,情况如下,注意cur指针位置
deque 操作
这方面真的和vector等很像。直接看书上的总结图吧。
它的 nonmodifying operation 。
参考书籍
《C++标准库-自学教程与参考手册》
《C++源码剖析》