除了我们常见的的
vector
和list
之外,还有一种序列式容器deque
。它是一种双向开口的连续线性空间,双向开口意味着你可以在它的头部,尾部任意插入元素,连续线性意味着它底层如同vector
那样是“连续”的空间。
我们知道 vector
底层是一块连续的空间, 正因为如此它的迭代器及其简单,仅仅是一个 T*
类型的指针,它的缺点也显而易见——头插数据的效率不高,空间“增长”的花费也很昂贵(申请新空间-搬移数据-释放旧空间)。而 deuqe
特殊的空间构造则巧妙的避免的 vector
的头插的缺点。
实际上,它的底层是一段一段的定量的连续空间(我们可以称这些一段一段的连续空间为数据缓冲区),而这些数据缓冲区则由一个被称为中控器的结构和两个迭代器来维护。
它们之间繁琐的关系让人分析起来无从下手,但其实只要稍稍一整理,我们就可以的出:
- 整块空间由一个
T**
的指针(map
)和两个迭代器维护(start
和finish
)。map
指向一段定长的连续线性空间,这些空间中储存的是一个个指向“数据缓冲区”的指针。(map如同一个指针数组)start
迭代器指的cur
向第一个缓冲区的第一个元素,finish
迭代器的cur
指向最后一个缓冲区的最后一个元素的下一个元素。即:start
“指向”“整块空间”的第一个元素,finish
“指向”“整块空间”的最后一个元素。- 数据实际上是储存在数据缓冲区中,而中控器只是负责把多个缓冲区“链接起来”, 从而造成“整块空间连续的假象”, 而
start
和finish
则标记着有效数据的空间的头和尾。
通过上面一系列的分析,我们知道, deuqe
所谓的“连续的空间”只是一种假象,实际上它是通过中控器将多段连续空间拼接在一起。
下面是 STL
库中的相关源码(STL3.0):
...
template <class T, class Alloc = alloc, size_t BufSiz = 0>
class deque {
public: // Basic types
typedef T value_type;
typedef value_type* pointer;
...
typedef pointer* map_pointer;
...
protected: // Data members
iterator start;
iterator finish;
map_pointer map;
size_type map_size;
源码中除了管理我们前面所说的一个指针两个迭代器之外,还有一个标记map大小的变量。
通过以上的学习,我们知道 deque
的结构与 vector
相比颇为复杂,同样的,它的迭代器结构也较复杂。
下面是迭代器相关的源码:
...
T* cur;
T* first;
T* last;
map_pointer node;
//迭代器的构造函数
__deque_iterator(T* x, map_pointer y)
: cur(x), first(*y), last(*y + buffer_size()), node(y) {}
__deque_iterator() : cur(0), first(0), last(0), node(0) {}
__deque_iterator(const iterator& x)
: cur(x.cur), first(x.first), last(x.last), node(x.node) {}
...
void set_node(map_pointer new_node) {
node = new_node;
first = *new_node;
last = first + difference_type(buffer_size());
}
通过代码,我们很容易参透该迭代器的奥秘:
map
: 当前迭代器标识的是哪个缓冲区;
first
: 指向当前缓冲区的头部;
last
:指向当前缓冲区的尾部;
cur
:指向当前缓冲区的某个具体数据。
该迭代器通过上面 cur
成员就可以精准的定位到该结构中的具体某个数据,不但如此, start
、 finish
和 node
之间的相互配合得以让该迭代器在不同数据缓冲区之间跳跃,这一特性将使得迭代器的自增、自减操作简单许多。
下面是它的自增、自减源码:
...
self& operator++() {
++cur;
if (cur == last) {
set_node(node + 1);
cur = first;
}
return *this;
}
self operator++(int) {
self tmp = *this;
++*this;
return tmp;
}
self& operator--() {
if (cur == first) {
set_node(node - 1);
cur = last;
}
--cur;
return *this;
}
self operator--(int) {
self tmp = *this;
--*this;
return tmp;
}
...
//该函数让当前迭代器跳到一个新的缓冲区(new_node)上
void set_node(map_pointer new_node) {
node = new_node;
first = *new_node;
last = first + difference_type(buffer_size());
}
...
显然,在进行自增操作后,如果 cur
已经到了结尾, 则让迭代器跳到下一个缓冲区上,并且让 cur
指向下一个缓冲区的头部(符合左闭右开原则)。同样的,在自减前,如果 cur
指向当前缓冲区的头部,则先跳到前一个缓冲区上,再让 cur
指向前一个缓冲区的尾部(仍符合左闭右开)。
下面的图将有助于理解自减这一过程:
deque
的尾插操作和 vector
极为相似,不同的是扩容处理。我们知道当 vector
没有预留空间时,会重新找一块更大的空间,然后拷贝数据,释放原空间,而 deque
却不同。它是怎样做的呢?如下:先插入三个数据:
push_back(1);
push_back(2);
push_back(3);
再插入四个个数据:
push_back(4);
push_back(5);
push_back(6);
push_back(7);
此时它的数据情况如下图左侧的,然后再插入一个数据后, finish
迭代器将发生变化,如下:
可以看出当把第一个缓冲区插满时,此时,先申请一块新的缓冲区,然后让当前 node
的下一个节点指向它,再调整 finish
迭代器。现在 finish
迭代器指向的是下一个缓冲区的起始位置,这也符合我们所说的,finish
指向最后一个元素的下一个位置,从表面来看,也使得 deque
如同“一段连续的空间”。
以上就是数据缓冲区的扩容,在有些情况下,中控器也需要扩容。中控器的扩容类似于 vector
的扩容,当中控器中的所有节点都已被分配缓冲区时,且边界的缓冲区已满的情况下再插入数据时,就需要给中控器扩容。
push_front()
头插数据和尾插数据如出一辙。这里不再详述。
pop_back()、 pop_front()
删除数据再简单不过了。头删数据的做法是让 start
迭代器的 cur
后移一个位置,若已经到了缓冲区结尾,则释放该缓冲区,并且让 start
跳到下一个缓冲区,cur
指向其起始位置。如果删除后整个 deque
已经没有数据,则释放掉 map
。
尾删也是如此,只不过是 cur
向前走。
下面给出在头部和尾部插入和删除数据的部分源代码(更多详细源码可至STL源码下载网站下载,鉴于3.0之后的版本源码命名风格,其阅读成本稍大,建议下载3.0之前的,仅供学习):
void push_back(const value_type& t) {
//如果没到缓冲区尾部,则世界插入数据
if (finish.cur != finish.last - 1) {
construct(finish.cur, t);
++finish.cur;
}
//否则调用备用插入函数进行插入
else
push_back_aux(t);//
}
void push_front(const value_type& t) {
if (start.cur != start.first) {
construct(start.cur - 1, t);
--start.cur;
}
else
push_front_aux(t);
}
void pop_back() {
//如果被删除数据不是缓冲区的第一个数据,则直接删除
if (finish.cur != finish.first) {
--finish.cur;
destroy(finish.cur);
}
//否则调用备用删除方法
else
pop_back_aux();
}
void pop_front() {
if (start.cur != start.last - 1) {
destroy(start.cur);
++start.cur;
}
else
pop_front_aux();
}
...
//以下是备用的增删函数
template <class T, class Alloc, size_t BufSize>
void deque ::push_back_aux(const value_type& t) {
value_type t_copy = t;
reserve_map_at_back();
//给下一个缓冲区分配空间
*(finish.node + 1) = allocate_node();
__STL_TRY {
//插入数据
construct(finish.cur, t_copy);
//将 `finish` 调整到下个缓冲区起始位置
finish.set_node(finish.node + 1);
finish.cur = finish.first;
}
__STL_UNWIND(deallocate_node(*(finish.node + 1)));
}
// Called only if finish.cur == finish.first.
template <class T, class Alloc, size_t BufSize>
void deque :: pop_back_aux() {
//释放最后一个缓冲区
deallocate_node(finish.first);
//调整 `finish`使其指向上个缓冲区的最后一个数据
finish.set_node(finish.node - 1);
finish.cur = finish.last - 1;
//析构该数据
destroy(finish.cur);
}
...
deque
复杂的结构和迭代器设计使得我们在平常很少使用它,而相对简单的 vector
则被频繁的使用,当然这个要视场景而定。
实际上,stack
和 queue
默认使用的容器就是 deque
。
—— 谢谢!
deque
.【作者:果冻 http://blog.csdn.net/jelly_9】