参考侯捷《STL源码剖析》
vector是单向开口的连续线性空间,deque则是一种双向开口的连续线性空间。所谓双向开口,意思是可以在头尾两端分别做元素的安插和删除动作。vector当然也可以在头尾两端做动作,但是其头部动作效率奇差,无法被接受。
deque和vector的最大差异,一在于deque允许在常数时间内对头尾端进行元素的安插或移除动作。二在于deque没有所有容量概念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。
虽然deque也提供Ramdon Access Iterator,但它的迭代器并不是普通指针,其复杂度和vector不可以并论,这当然在影响了各个运算层面。因此,除非必要,我们应尽量选择使用vector而非deque。
小技巧:对deque进行的排序,为了提高效率,可将deque先完整复制到一个vector身上,将vector排序后,再复制到deque.
deque是连续空间,连续线性空间总令我们联想到array或vector。array无法成长,vector虽可成长,却只能向尾端成长,而且其所谓成长原是个假象,事实上是(1)另寻找更大空间,(2)将原数据复制过去,(3)释放原来空间三部曲。如果不是vector每次配置新空间都留下一些富裕,其成长假象所带来的代价是相当高昂。
deque是由一段一段的定量连续空间构成。一旦有必要在deque的前端或尾端增加新空间,便配置一段定量连续空间,串接在整个deque的头端或尾端。deque的最大任务,便是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取的界面。避开了“重新配置,复制,释放”的轮回,代价则是复杂的迭代器架构。
受到分段连续线性空间的字面影响,我们可能以为deque的操作复杂度和vector相差不多。其实不是这样,虽然是分段连续线性空间,就必须有中央控制,而为了维护整体连续的假象,数据结构的设计及迭代器前进后退等动作颇为繁琐。deque的操作代码量远比vector或list都多得多。这也就是deque最主要的缺点。
deque采用一块所谓的map,(不是STL的map容器)作为主控。这里所谓map是一小段连续空间,其中每个元素(此处称为一个节点,node)都是指标,指向另一段较大的连续线性空间,称为缓冲区。缓冲区才是deque的存储空间主体。SGI STL允许我们制定缓冲区大小,默认值0表示将使用512bytes缓存区。
map是块连续空间,其内的每个元素都是一个指针(称为节点),指向一块缓冲区;map其实就是一个T**,也就是它是一个指针,所指之物又是一个指针,指向型别为T的一块空间,如下图所示。
deque是分段连续空间。维护其整体连续假象的任务,这就落在迭代器的operator++和operator–两个运算子上。
deque迭代器应该具备什么结构。首先,他必须能够指出分段连续空间(亦即缓冲区)在哪里,其次他必须能够判断自己是否已经处于其所在缓冲区的边缘,如果是,一旦前进或后退时必须跳跃至下一个或上一个缓存区。为了能够正确跳跃,deque必须随时掌握管控中心(map)。
所以迭代器在前进和后退时就需要判断是否为当前缓冲区的最后一个元素或者是第一个元素。
// __deque_iterator的数据结构
template <class T, class Ref, class Ptr, size_t BufSiz>
struct __deque_iterator
{
typedef __deque_iterator<T, T&, T*> iterator;
typedef __deque_iterator<T, const T&, const T*> const_iterator;
static size_t buffer_size() {return __deque_buf_size(0, sizeof(T)); }
typedef random_access_iterator_tag iterator_category;
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef T** map_pointer;
typedef __deque_iterator self;
// 保持与容器的联结,是对某一个缓冲区而言的
T* cur; // 此迭代器所指之缓冲区中的现行元素
T* first; // 此迭代器所指之缓冲区的头
T* last; // 此迭代器所指之缓冲区的尾(含备用空间)
map_pointer node; // 指向管控中心
...
}
template <class T, class Alloc = alloc, size_t BufSiz = 0>
class deque {
.....
protected: // Data members
iterator start; //指向第一个缓冲区
iterator finish; //指向最后一个缓冲区
map_pointer map; //指向map,map是块连续空间
size_type map_size; //map内有多少个指针
.....
deque除了维护一个指向map的指针外,也维护start,finish两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素的下一位置,此外,它当然也必须记住目前map的大小,因为一旦map所提供的节点不足,就必须重新配置更大一块的map.
一个map管理的节点数为至少为8个,至多是所需要的节点数+2,以供deque前后各预留一个,扩充时就可直接使用;而且初始的start和finish要指向map所拥有之全部节点的最中央区段,这样的话,就可以是头尾两端所能扩充的空间一样大。
注意:当map节点的备用空间不足时,比如尾端空间不足,即使头部有大量剩余也不会往头部进行迁移,而是重新开辟一块map,新map的大小为:size_type new_map_size = map_size + max(map_size, nodes_to_add) + 2;
node_to_add是你想要增加的节点数量,也就是缓冲区数量,默认是1;map_size是原来的maop的大小,也就是节点的数量。+2是头端和尾端都预留一个。
关键代码:push_back
void push_back(const value_type& t) {
//最后一个缓冲区有两个及以上的空间时
if (finish.cur != finish.last - 1) {
//直接构造元素
construct(finish.cur, t);
++finish.cur;
}
//最后一个缓冲区只剩一个空间,那么就需要配置新的缓冲区了
else
push_back_aux(t);
}
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::push_back_aux(const value_type& t) {
value_type t_copy = t;
//若符合某种条件需要更改map
reserve_map_at_back();
//这就是配置一个新的节点(也就是新的一个缓冲区)
*(finish.node + 1) = allocate_node();
__STL_TRY {
construct(finish.cur, t_copy);
finish.set_node(finish.node + 1);
finish.cur = finish.first;
}
__STL_UNWIND(deallocate_node(*(finish.node + 1)));
}
关键代码:pop_back
void pop_back() {
//最后缓冲区有一个或多个元素
if (finish.cur != finish.first) {
--finish.cur;//调整指针
destroy(finish.cur);//将最后元素析构掉
}
//最后一个缓冲区没有一个元素
else
//进行缓冲区的释放
pop_back_aux();
}
只有当finish.cur == finish.first 时才会被调用
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>:: pop_back_aux() {
deallocate_node(finish.first); //释放最后一个缓冲区
finish.set_node(finish.node - 1); //finish指向上一个节点
finish.cur = finish.last - 1; //指向上一个缓冲区的最后一个元素
destroy(finish.cur); //将该元素析构
}
关键代码(清除整个deque)
clear()函数用来清除整个deque。需要注意的是,deque的初始状态(无任何元素时)保有一个缓冲区,因此,clear()完成之后恢复初始状态,也一样要保留一个缓冲区。
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::clear() {
//对于除头尾以外的每一个缓冲区,对元素进行析构,并且释放缓冲区内存
for (map_pointer node = start.node + 1; node < finish.node; ++node) {
destroy(*node, *node + buffer_size());
data_allocator::deallocate(*node, buffer_size());
}
//将头缓冲区的元素析构掉,并且释放缓冲区内存
if (start.node != finish.node) {
destroy(start.cur, start.last);
destroy(finish.first, finish.cur);
//释放尾缓冲区内存
data_allocator::deallocate(finish.first, buffer_size());
}
//析构尾缓冲区中的元素,但不释放缓冲区空间,也就是一个唯一的缓冲区
else
destroy(start.cur, finish.cur);
//调整状态,start和finish现在指向同一个缓冲区
finish = start;
}
测试:
class TyTest{
public:
TyTest()
{
}
TyTest(const TyTest &o)
{
a = o.a;
cout << "这是拷贝构造函数,a:" << a << endl;
}
~TyTest()
{
cout << "这是析构函数\n";
}
int a{0};
};
void main()
{
deque<TyTest> m_deque;
TyTest t1;
t1.a = 1;
m_deque.push_back(t1);
TyTest t2;
t2.a = 2;
m_deque.push_back(t2);
TyTest t3;
t3.a = 3;
m_deque.push_back(t3);
TyTest t4;
t4.a = 4;
m_deque.push_back(t4);
TyTest t5;
t5.a = 5;
m_deque.push_back(t5);
TyTest t6;
t6.a = 6;
m_deque.push_back(t6);
TyTest t7;
t7.a = 7;
m_deque.push_back(t7);
cout << "插入之前遍历:\n";
for (auto &node :m_deque)
{
cout << node.a << " ";
}
cout << endl;
//现在在t4前插入一个元素
auto it = find_if(m_deque.begin(), m_deque.end(), [](TyTest &t){return t.a == 4; });
if (it != m_deque.end())
{
cout << "find:"<<it->a << endl;
}
cout << "我要插入了\n";
TyTest t8;
t8.a = 8;
m_deque.insert(it, t8);
cout << "插入之后遍历:\n";
for (auto &node : m_deque)
{
cout << node.a << " ";
}
cout << endl;
system("pause");
}