deque容器详解,包含源码
Posted 两片空白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了deque容器详解,包含源码相关的知识,希望对你有一定的参考价值。
目录
一.概述
vector是一个单向开口的连续线性空间,deque则是一个双向开口的连续线性空间。所谓双向开口,意思是可以在头尾两端分别做元素的插入和删除操作。
vector当然也可以进行头尾两端的插入和删除操作,但是头部的插入和删除操作效率极差。需要移动元素。
deque和vector差异:
- deque允许常数时间内对其头端进行元素的插入和删除操作。
- deque没有所谓容量的概念。因为它是动态的以分段连续空间组合而成,随时可以增加一段新的空间并连接起来。不会像vector那样,因为空间不足而扩容,复制元素到新的空间,再释放旧的空间。因此deque没有必要提供所谓的扩容(reserve)的功能。
虽然deque也提供了迭代器,但是它的迭代器并不是普通的指针,实现比vector复杂很多,这当然影响了deque的各个运算层面。因此,除非必要,应尽可能的选择使用vector而非deque。
对deque的排序操作,为了更高的效率,可将deque先完整的复制到一个vector中,将vector排序后,在复制回deque。
deque为什么可以很好的实现头插和尾插,是因为deque结构中有两个迭代器:start和finish。
二.deque的中控器
deque从逻辑上,使用上看是一个连续的空间。实际是由一段一段定量连续空间构成。一旦有必要在deque前端或者尾端增加新空间,便配置一段定量连续空间,串联在整个deque的头端或者尾端。
deque最大的任务是在这些分段的定量连续空间上,维护其整体连续的假象。即,在使用上就像一个连续的整体。并提供随机存取的接口。避开了“重新配置,复制,释放”的轮回,代价是复杂的迭代器结构。
由于deque是分段连续线性空间,就必须有中央控制。deque采用一块所谓了类似map的结构作为主控。这里的map实际是一个连续的空间,里面保存的都是指针,指向另一端较大的连续线性空间,称为缓冲区。缓冲区才是deque存储数据的空间主体。
这里的map实际是一个数组,数组里的数据是缓冲区(保存数据的实体)的起始地址。由于deque可以在起始插入元素,也可以从尾部插入元素,所以map是从中间开始保存缓冲区的起始地址。当在起始位置插入元素超过一个缓冲区的范围,新增一个缓冲区,则接着保存在map的前面,反之则接着保存在map的后面。当map保存的向前或者向后保存的数据超过了map的范围,则需要扩容。
三.deque的迭代器
迭代器的结构主要包含下面几个成员变量:
cur指向当前访问元素。
first指向当前缓冲区的第一个元素。
last指向当前缓冲区的最后一个元素。
node保存的中控数组map中保存当前缓冲区地址节点的地址。
deque是分段连续空间。维持其整体连续假象的任务。落在了迭代器的"operator++"和"operator--"两个重载运算符上。
deque的迭代器首先必须能够指出是哪个缓冲区。其次,必须可以判断出当前是否处于缓冲区的边缘。如果是,则需要调到下一个或者上一个缓冲区。为了能够正确的跳跃,deque必须随时掌握控制中心(map)。
源码成员变量如下:
#ifndef __STL_NON_TYPE_TMPL_PARAM_BUG
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));
#else /* __STL_NON_TYPE_TMPL_PARAM_BUG */
template <class T, class Ref, class Ptr>
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));
#endif
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; //指向中控器中保存缓冲区地址位置的地址
其中缓冲区的大小的函数buffer_size(),调用__deque_buf_size(),定义如下:
- 如果n不为0,传回n,表示用户自己定义。
- 如果n为0,传默认值。如果sz( 元素大小,szieof(T) )小于512,传回512/sz,否则传回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));
迭代器的begin()和end(),所传回的两个迭代器实际保存在deque的结构中,名为start和finish。在下面deque数据结构中可以看到。
假设现在我们产生一个元素类型为int,缓冲区大小为8的缓冲区,语法形式为deque<int, alloc, 8>。结果某些操作后,deque拥有20个元素。20个元素需要3个缓冲区。所以中控器map内使用了3个节点。迭代器start的cur指向第一个缓冲区的第一个元素。迭代器的finish的cur指向最后一个缓冲区的最后一个元素,不一定是last的位置。
deque::begin()返回迭代器start,deque::end()返回迭代器finish。这两个结构都保存在deque的结构中。
注意:
- 中控器map的起始大小为8。
- 最后一个缓冲区有备用空间,如果之后要在尾部插入数据,可以直接使用备用空间。
- 调到下一个缓冲区,即让成员变量node指向下一个缓冲区(看下面实现),赋值first,last和cur。
deque使用起来像整体连续的顺序结构,是因为迭代器重载了operator++和operator--操作,和以下几个关键的操作。主要是各种指针的加,减,前进和后退都不能直接视之,其中最关键的是:一旦行进时遇到缓冲区边缘,需要视前进和后退而定,可能需要调用set_node(),调到下一个缓冲区。
在源代码中set_node()实现如下:
//node保存的是中控器中保存当前缓冲区地址的地址,即中控器中某节点的地址,不是缓冲区的地址
//传入连续的下一个缓冲区,设置好迭代器的first和last的值。
void set_node(map_pointer new_node)
node = new_node;
first = *new_node;
last = first + difference_type(buffer_size());
一下各个重载函数是deque迭代器成功运行的关键:
reference operator*() const return *cur;
pointer operator->() const return &(operator*());
//首先计算出在中控器中相距字节数,在计算在this缓冲区和x缓冲区,cur里last的字节数
//加起来为总字节数
difference_type operator-(const self& x) const
return difference_type(buffer_size()) * (node - x.node - 1) +
(cur - first) + (x.last - x.cur);
//前置++
self& operator++()
++cur;
if (cur == last) //判断是否走到边缘
set_node(node + 1); //中控器的下一个位置
cur = first; //cur指向第一个元素
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;
self& operator+=(difference_type n)
difference_type offset = n + (cur - first);
if (offset >= 0 && offset < difference_type(buffer_size()))
//在同一缓冲区中
cur += n;
else
//不在同一缓冲区
//计算离现在缓冲区的个数,向前或者向后
difference_type node_offset =
offset > 0 ? offset / difference_type(buffer_size())
: -difference_type((-offset - 1) / buffer_size()) - 1;
//切换到正确缓冲区
set_node(node + node_offset);
//切换到正确元素
cur = first + (offset - node_offset * difference_type(buffer_size()));
return *this;
self operator+(difference_type n) const
self tmp = *this;
return tmp += n;
self& operator-=(difference_type n) return *this += -n;
self operator-(difference_type n) const
self tmp = *this;
return tmp -= n;
reference operator[](difference_type n) const return *(*this + n);
bool operator==(const self& x) const return cur == x.cur;
bool operator!=(const self& x) const return !(*this == x);
bool operator<(const self& x) const
return (node == x.node) ? (cur < x.cur) : (node < x.node);
四.deque的数据结构
前面有提到,deque除了维护一个中控器map指针外,也维护了start和finish迭代器。分别指向第一个缓冲区的第一个元素和最后一个缓冲区的最后一个元素的下一个地址。此外还需要记住目前map大小,因为一旦map所提供的节点不足,就需要重新配置一块更大的map,需要记录map节点个数,将旧map里的值拷贝过去。
template <class T, class Alloc = alloc, size_t BufSiz = 0>
class deque
public: // Basic types
typedef T value_type;
typedef value_type* pointer;
typedef const value_type* const_pointer;
typedef value_type& reference;
typedef const value_type& const_reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
public: // Iterators
typedef __deque_iterator<T, T&, T*, BufSiz> iterator;
typedef __deque_iterator<T, const T&, const T&, BufSiz> const_iterator;
typedef reverse_iterator<const_iterator> const_reverse_iterator;
typedef reverse_iterator<iterator> reverse_iterator;
protected: // Internal typedefs
//元素指针的指针,即数组里保存的指针
typedef pointer* map_pointer;
//中控器默认大小为8
static size_type initial_map_size() return 8;
protected: // Data members
iterator start; //第一个节点的迭代器
iterator finish;//最后一个节点的迭代器
map_pointer map; //中控器
size_type map_size;//中控器中有多少指针
...
有了以上结构就可以轻易完成下面操作:
public: // Basic accessors
iterator begin() return start;
iterator end() return finish;
const_iterator begin() const return start;
const_iterator end() const return finish;
reverse_iterator rbegin() return reverse_iterator(finish);
reverse_iterator rend() return reverse_iterator(start);
const_reverse_iterator rbegin() const
return const_reverse_iterator(finish);
const_reverse_iterator rend() const
return const_reverse_iterator(start);
//调用迭代器重载的operator[]
reference operator[](size_type n) return start[difference_type(n)];
const_reference operator[](size_type n) const
return start[difference_type(n)];
//调用迭代器重载的operator*
reference front() return *start;
reference back()
iterator tmp = finish;
--tmp;
return *tmp;
const_reference front() const return *start;
const_reference back() const
const_iterator tmp = finish;
--tmp;
return *tmp;
size_type size() const return finish - start;;
size_type max_size() const return size_type(-1);
bool empty() const return finish == start;
五.deque的构造和内存管理
5.1 deque的构造
此时构造了一个deque,缓冲区默认大小为8,并是用20个空间,初始值为9。图示如下:
deque自行定义了两个专属的控制配置器:
//配置元素的大小
typedef simple_alloc<value_type, Alloc> data_allocator;
//配置指针大小
typedef simple_alloc<pointer, Alloc> map_allocator;
并提供了一个构造函数:
deque(int n, const value_type& value)
: start(), finish(), map(0), map_size(0)
fill_initialize(n, value);
调用的fill_initialize()函数复制产生好deque结构,并将元素初值设置妥当。
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::fill_initialize(size_type n,
const value_type& value)
create_map_and_nodes(n);//把deque结构产生并安排好
map_pointer cur;
__STL_TRY
//为每个节点的缓冲区节点设置初值
for (cur = start.node; cur < finish.node; ++cur)
uninitialized_fill(*cur, *cur + buffer_size(), value);
//最后一个缓冲区设定不同,因为尾端可能有备份空间,不需要设置初值。
uninitialized_fill(finish.first, finish.cur, value);
#ifdef __STL_USE_EXCEPTIONS
catch(...)
for (map_pointer n = start.node; n < cur; ++n)
destroy(*n, *n + buffer_size());
destroy_map_and_nodes();
throw;
#endif /* __STL_USE_EXCEPTIONS */
void deque<T, Alloc, BufSize>::create_map_and_nodes(size_type num_elements)
//需要的中控器节点数
//如果刚好除整,会多分配一个节点
size_type num_nodes = num_elements / buffer_size() + 1;
//中控器的节点数。最少默认值8个,最多需要节点数加2
//前后预留一个,便于扩充
map_size = max(initial_map_size(), num_nodes + 2);
map = map_allocator::allocate(map_size);
//令nstart和nfinish指向中控器使用节点的头尾
map_pointer nstart = map + (map_size - num_nodes) / 2;
map_pointer nfinish = nstart + num_nodes - 1;
map_pointer cur;
__STL_TRY
//为中控器中使用了的节点分配缓冲区
for (cur = nstart; cur <= nfinish; ++cur)
*cur = allocate_node();
#ifdef __STL_USE_EXCEPTIONS
catch(...)
for (map_pointer n = nstart; n < cur; ++n)
deallocate_node(*n);
map_allocator::deallocate(map, map_size);
throw;
#endif /* __STL_USE_EXCEPTIONS */
//设置start和finish迭代器
start.set_node(nstart);
finish.set_node(nfinish);
start.cur = start.first;
finish.cur = finish.first + num_elements % buffer_size();
5.2 内存处理
注意
- 更新start个finish迭代器调用的是重载的operator函数,里面考虑了跨缓冲区的操作。
- copy(a,b,c)移动元素,是将[a,b]区间的值,移动到区间以c位置开始。
- copy_backward(a,b,c)移动元素,是将[a,b]区间的值,移动到区间以c结尾的区间。
- push_back操作
元素重新赋值,并在尾插3个元素 :
deque的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);
现在再新增一个元素在尾端,ideq.push_back(3),此时push_back会调用push_back_aux(),先配置一块新的缓冲区,在设置行元素内容,然后更新迭代器finish的状态。
尾插,如果最后一个缓冲区只剩下一个备用空间时,就新增缓冲区。
//最后一个缓冲区只剩下一个元素时调用
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;
reserve_map_at_back(); //判断是否更换map
*(finish.node + 1) = allocate_node(); //配置新节点
__STL_TRY
construct(finish.cur, t_copy); //设置
finish.set_node(finish.node + 1); //更新finish
finish.cur = finish.first;
__STL_UNWIND(deallocate_node(*(finish.node + 1)));
- push_front操作
现在往前面插入一个元素:调用push_front(99),deque的push_front实现
void push_front(const value_type& t)
//第一个缓冲区还有备用空间
if (start.cur != start.first)
construct(start.cur - 1, t);//直接构建元素插入
--start.cur;
else
push_front_aux(t);
当第一个缓冲区没有备用空间,进行头插时,会调用push_front_aux(),新增缓冲区。
// Called only if start.cur == start.first.
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::push_front_aux(const value_type& t)
value_type t_copy = t;
reserve_map_at_front(); //判断中控器是否需要扩容
*(start.node - 1) = allocate_node(); //分配缓存区
__STL_TRY
start.set_node(start.node - 1); //设置start
start.cur = start.last - 1;
construct(start.cur, t_copy); //设置好插入元素
# ifdef __STL_USE_EXCEPTIONS
catch(...)
start.set_node(start.node + 1); //不成功还原
start.cur = start.first;
deallocate_node(*(start.node - 1));
throw;
# endif /* __STL_USE_EXCEPTIONS */
如果在头插元素,有序第一个缓冲区有备用空间,可以直接插入到备用空间中。
- 中控器的扩容
两种情况,头插和尾插新增缓冲区时,节点不够需要扩容。分别调用的是reserve_map_at_front和reserve_map_at_back,但是实际是调用reallocate_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);
void reserve_map_at_front (size_type nodes_to_add = 1)
if (nodes_to_add > start.node - map)
//中控器前端节点空间不足
reallocate_map(nodes_to_add, true);
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::reallocate_map(size_type nodes_to_add,
bool add_at_front)
size_type old_num_nodes = finish.node - start.node + 1;
size_type new_num_nodes = old_num_nodes + nodes_to_add;
map_pointer new_nstart;
if (map_size > 2 * new_num_nodes)
new_nstart = map + (map_size - new_num_nodes) / 2
+ (add_at_front ? nodes_to_add : 0);
if (new_nstart < start.node)
copy(start.node, finish.node + 1, new_nstart);
else
copy_backward(start.node, finish.node + 1, new_nstart + old_num_nodes);
else
size_type new_map_size = map_size + max(map_size, nodes_to_add) + 2;
//配置一块空间给中控器
map_pointer new_map = map_allocator::allocate(new_map_size);
new_nstart = new_map + (new_map_size - new_num_nodes) / 2
+ (add_at_front ? nodes_to_add : 0);
//把旧中控器内容拷贝到新中控器
copy(start.node, finish.node + 1, new_nstart);
//释放旧中控器
map_allocator::deallocate(map, map_size);
map = new_map;
map_size = new_map_size;
//重新设置start和finish
start.set_node(new_nstart);
finish.set_node(new_nstart + old_num_nodes - 1);
- pop_front操作
作用是从deque前端拿掉一个元素,注意需要考虑是否需要将缓冲区释放。
void pop_front()
if (start.cur != start.last - 1)
//第一个缓冲区有两个以上元素
destroy(start.cur);//第一个元素析构
++start.cur; //修改start的cur值
else
//第一个缓冲区只有一个元素
pop_front_aux();
当第一个缓冲区只有一个元素时,将元素pop出去后,对第一个缓冲区进行释放。
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::pop_front_aux()
destroy(start.cur); //将第一个缓冲区的元素释放,只有一个元素
deallocate_node(start.first); //将缓冲区释放
start.set_node(start.node + 1);//修改start
start.cur = start.first;
- pop_back操作
作用是从deque尾端那掉一个元素,注意需要考虑缓冲区释放的情况。
void pop_back()
if (finish.cur != finish.first)
//最后一个缓冲区有一个以上元素
--finish.cur; //更新finish的cur
destroy(finish.cur); //析构元素
else
//最后一个缓冲区没有元素
pop_back_aux();
当最后一个缓冲区没有元素,对最后一个缓冲区进行释放,然后再pop元素。
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); //析构元素
- 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); //销毁缓冲区里的所有元素
finish = start;
- erase清除某个元素操作
清除当前迭代器的元素。
iterator erase(iterator pos)
iterator next = pos;
++next;
difference_type index = pos - start;//清除点前的元素个数
if (index < (size() >> 1))
//如果清除位置之前的元素少
copy_backward(start, pos, next); //移动前面的元素
pop_front(); //第一个元素冗余了,去除,还实现了缓存释放
else
//如果清除位置之后的元素少
copy(next, finish, pos); //移动后面的元素
pop_back(); //最后一个元素冗余了,去除,还实现了缓存释放
return start + index;
- erase清除一段内的元素
template <class T, class Alloc, size_t BufSize>
deque<T, Alloc, BufSize>::iterator
deque<T, Alloc, BufSize>::erase(iterator first, iterator last)
//清除所有
if (first == start && last == finish)
clear();
return finish;
else
difference_type n = last - first; //清除区间元素个数
difference_type elems_before = first - start; //清除区间前的元素个数
if (elems_before < (size() - n) / 2) //清除区间前元素个数少
copy_backward(start, first, last); //移动前面的元素,往后移,覆盖清除区间
iterator new_start = start + n; //获得新起点
destroy(start, new_start); //销毁冗余元素
for (map_pointer cur = start.node; cur < new_start.node; ++cur)
data_allocator::deallocate(*cur, buffer_size()); //销毁冗余缓冲区
start = new_start;
else
copy(last, finish, first); //清除区间后元素少,区间后面元素向前移
iterator new_finish = finish - n; //更新finish
destroy(new_finish, finish); //清除冗余元素
for (map_pointer cur = new_finish.node + 1; cur <= finish.node; ++cur)
data_allocator::deallocate(*cur, buffer_size());//清除冗余缓冲区
finish = new_finish;
return start + elems_before;
- insert在某位置前插入元素
iterator insert(iterator position, const value_type& x)
if (position.cur == start.cur)
//头插
push_front(x);
return start;
else if (position.cur == finish.cur)
//尾插
push_back(x);
iterator tmp = finish;
--tmp;
return tmp;
else
return insert_aux(position, x);
template <class T, class Alloc, size_t BufSize>
typename deque<T, Alloc, BufSize>::iterator
deque<T, Alloc, BufSize>::insert_aux(iterator pos, const value_type& x)
difference_type index = pos - start; //插入点前的元素
value_type x_copy = x;
if (index < size() / 2)
//前面的元素少
push_front(front()); //头插于第一个元素相同的值,移动位置是需要得到该迭代器
iterator front1 = start; //做标记
++front1;
iterator front2 = front1;
++front2;
pos = start + index;
iterator pos1 = pos;
++pos1;
copy(front2, pos1, front1); //插入位置前元素移动出插入位置空隙
else
push_back(back()); //尾插一个于最后元素一样的值,移动位置是需要得到该迭代器
iterator back1 = finish; //做标记
--back1;
iterator back2 = back1;
--back2;
pos = start + index;
copy_backward(pos, back2, back1); //元素移动
*pos = x_copy; //在插入位置设置进新值
return pos;
六. deque的优缺点
优点:
由于deque是分段式连续结构,并且deque结构中保存了start和finish迭代器,分别指向第一个缓冲区和最后一个缓冲区。实现了头插和尾插的时间复杂度为常数次。
缺点:
由于deque里复杂的迭代器的实现,导致deque在遍历上,即需要遍历中控器查找每一个缓冲区,有需要遍历没有个缓冲区的每一个元素。效率不高。
所以,在stl中,deque是作为容器适配器stack和queue的默认容器,因为他们不需要遍历。
参考:stl源码剖析
以上是关于deque容器详解,包含源码的主要内容,如果未能解决你的问题,请参考以下文章
STL 源码剖析读书笔记四:序列式容器之 dequestackqueue
deque容器系列一基于STL源码分析deque容器整体实现及内存结构