deque容器

deque 和 vector 的最大差异一在于 deque 允许常数时间内对头端或尾端进行元素的插入或移除操作二在于 deque 没有所谓的容量概念,因为它是动态地以分段连续空间组合而成随时可以增加一块新的空间并拼接起来。虽然 deque 也提供随机访问的迭代器,但它的迭代器和vector、list容器的都不一样,其设计相当复杂度和精妙。

1. 中控器

deque 采用一块所谓的 map (注意不是STL里面的map容器)作为中控器,其实就是一小块连续空间,其中的每个元素都是指针,指向另外一段较大的连续线性空间,称之为缓冲区。

#ifndef __STL_NON_TYPE_TMPL_PARAM_BUG
template <class T, class Ref, class Ptr, size_t BufSiz>
class deque {
public:
      typedef T value_type;
      typedef value_type* pointer;
      ...
protected:
      typedef pointer** map_pointer;
      map_pointer map; // 指向 map,map 是连续空间,其内的每个元素都是一个指针。
      size_type map_size;
      ...
};

其示例图如下:deque 的结构设计中,map 和 node-buffer 的关系如下:

控制器map初始大小为8,用于存储缓冲区,当空间不够存储新的缓冲区时,就需要扩充map的空间,扩充方式与vector的扩充方式一致,以下是实现源码:

size_type _Newsize = 0 < this->_Mapsize ? this->_Mapsize : 1;
while (_Newsize - this->_Mapsize < _Count || _Newsize < _DEQUEMAPSIZ) // _DEQUEMAPSIZ = 8
{   // scale _Newsize to 2^N >= _Mapsize + _Count
    if (max_size() / _DEQUESIZ - _Newsize < _Newsize)
        _Xlen();    // result too long
    _Newsize *= 2;
}

每次扩容之后,原来的缓冲区,从map下标 oldsize/2 开始存放,上下预留相同的空行,方便支持deque首尾元素的添加

2. 迭代器

deque 是分段连续空间,维持其“整体连续”假象的任务,就靠它的迭代器来实现,也就是 operator++ 和 operator-- 两个运算子上面。

首先第一点,我们能想到的是,既然是分段连续,迭代器应该能指出当前的连续空间在哪里其次,第二点因为缓冲区有边界,迭代器还应该要能判断,当前是否处于所在缓冲区的边缘,如果是,一旦前进或后退,就必须跳转到下一个或上一个缓冲区;第三点,也就是实现前面两种情况的前提,迭代器必须能随时控制中控器。

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)); }
      // deque是random_access_iterator_tag类型
      typedef random_access_iterator_tag iterator_category;
      // 基本类型的定义, 满足traits编程
      typedef T value_type;
      typedef Ptr pointer;
      typedef Ref reference;
      typedef size_t size_type;
      typedef ptrdiff_t difference_type;
      // node
      typedef T** map_pointer;
      map_pointer node;
      typedef __deque_iterator self;
      ...
};

deque 的每一个缓冲区由设计了三个迭代器。

struct __deque_iterator {  
	... 
	typedef T value_type; 
	T* cur; // 此迭代器指向缓冲区现行元素
        T* first; // 此迭代器指向缓冲区的头
	T* last; // 此迭代器指向缓冲区的尾
	typedef T** map_pointer; 
	map_pointer node; // 指向管控中心
	...
};

下图描绘了deque 的中控器、缓冲区、迭代器之间的相互关系。其中 cur 表示当前所指的位置,first 表示当前数组中头的位置,last 表示当前数组中尾的位置。

 

那么,缓冲区大小是谁来决定的呢?这里呢,用来决定缓冲区大小的是一个全局函数:

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));
}
// 如果 n 不为0,则返回 n,表示缓冲区大小由用户自定义
// 如果 n == 0,表示 缓冲区大小默认值
// 如果 sz = (元素大小 sizeof(value_type)) 小于 512 则返回 512/sz
// 如果 sz 不小于 512 则返回 1

假设我们现在构造了一个 int 类型的 deque,设置缓冲区大小等于 32,这样一来,每个缓冲区可以容纳 32/sizeof(int) = 8(64位系统)个元素。经过一番操作之后,deque 现在有 20 个元素了,那么成员函数 begin() 和 end() 返回的两个迭代器应该是怎样的呢?如下图所示:

20 个元素需要 20/(sizeof(int)) = 5(图中只展示3个)个缓冲区。所以 map 运用了三个节点。迭代器 start 内的 cur 指针指向缓冲区的第一个元素,迭代器 finish 内的 cur 指针指向缓冲区的最后一个元素(的下一个位置)。

迭代器的操作:

set_node 跳出一个缓冲区。

void set_node(map_pointer new_node) {
    node = new_node;
    first = *new_node;
    last = first + difference_type(buffer_size());
}

操作符 - 两对象之间的相减,a - b获得从b到a的长度。

difference_type operator-(const self& x) const {
    return difference_type(buffer_size()) * (node - x.node - 1) + (cur - first) + (x.last - x.cur);
}
// 其中 (node - x.node - 1) 计算两根迭代器之间完整buffer的长度
// (cur - first) 计算末尾buffer的元素量
// (x.last - x.cur) 计算起始buffer的元素量

operator++ 操作代表是需要切换到下一个元素,这里需要先切换再判断是否已经到达缓冲区的末尾。

self& operator++() { 
      ++cur;      //切换至下一个元素
      if (cur == last) {   //如果已经到达所在缓冲区的末尾
         set_node(node+1);  //切换下一个节点
         cur = first;  
      }
      return *this;
}

operator-- 操作代表切换到上一个元素所在的位置,需要先判断是否到达缓冲区的头部,再后退。

self& operator--() {     
      if (cur == first) {    //如果已经到达所在缓冲区的头部
         set_node(node - 1); //切换前一个节点的最后一个元素
         cur = last;  
      }
      --cur;       //切换前一个元素
      return *this;
}

操作符 [ ]

reference operator[](size_type n){
	return start[difference_type(n)];
}

操作符 *

reference operator*() const{
	return *cur;
}

操作符 ->

pointer operator->() const{
	return &(operator*());
}

3. deque数据结构

deque除了维护一个指向map的指针外,也维护start,finish两个迭代器,分别指向第一个缓冲区的第一个元素和最后缓冲区的最后一个元素(的下一个位置)。此外,它也必须记住目前的map大小。

#ifndef __STL_NON_TYPE_TMPL_PARAM_BUG
template <class T, class Ref, class Ptr, size_t BufSiz>
class deque {
public:
    typedef T value_type;
    typedef value_type* pointer;
    typedef size_t size_type;
    ...
public:
    typedef _deque_iterator<T, T&, T*, BufSiz> iterator;
protected:
    typedef pointer** map_pointer;
protected:
    iterator start;
    iterator finish;
    map_pointer map; // 指向 map,map 是连续空间,其内的每个元素都是一个指针。
    size_type map_size;
    ...
};

4. 构造和析构

deque构造函数有多个重载函数, 接受大部分不同的参数类型. 基本上每一个构造函数都会调用create_map_and_nodes, 这就是构造函数的核心。

template <class T, class Alloc = alloc, size_t BufSiz = 0> 
class deque {
    ...
public:                         // Basic types
    deque() : start(), finish(), map(0), map_size(0){
        create_map_and_nodes(0);
    }  // 默认构造函数
    deque(const deque& x) : start(), finish(), map(0), map_size(0) {
        create_map_and_nodes(x.size());
        __STL_TRY {
            uninitialized_copy(x.begin(), x.end(), start);
        }
        __STL_UNWIND(destroy_map_and_nodes());
    }
    // 接受 n:初始化大小, value:初始化的值
    deque(size_type n, const value_type& value) : start(), finish(), map(0), map_size(0) {
        fill_initialize(n, value);
    }
    deque(int n, const value_type& value) : start(), finish(), map(0), map_size(0) {
        fill_initialize(n, value);
    } 
    deque(long n, const value_type& value) : start(), finish(), map(0), map_size(0){
        fill_initialize(n, value);
    }
    ...
};

deque 中控器的配置函数:

void deque<T,Alloc,BufSize>::create_map_and_nodes(size_type_num_elements) {
    //需要节点数= (每个元素/每个缓冲区可容纳的元素个数+1)
    //如果刚好整除,多配一个节点
    size_type num_nodes = num_elements / buffer_size() + 1;
    //一个 map 要管理几个节点,最少 8 个,最多是需要节点数+2
    map_size = max(initial_map_size(), num_nodes + 2);
    map = map_allocator::allocate(map_size);
    // 计算出数组的头前面留出来的位置保存并在nstart.
    map_pointer nstart = map + (map_size - num_nodes) / 2;
    map_pointer nfinish = nstart + num_nodes - 1;
    map_pointer cur;//指向所拥有的节点的最中央位置
    ...
}

注意deque 的 begin 和 end 不是一开始就是指向 map 中控器里开头和结尾的,而是指向所拥有的节点的最中央位置。这样带来的好处是可以使得头尾两边扩充的可能性和一样大,换句话来说,因为 deque 是头尾插入都是 O(1), 所以 deque 在头和尾都留有空间方便头尾插入。

那么,什么时候 map 中控器本身需要调整大小呢?触发条件在于 reserve_map_at_back 和 reserve_map_at_front 这两个函数来判断,实际操作由 reallocate_map 来执行。

// 如果 map 尾端的节点备用空间不足,符合条件就配置一个新的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);
}

// 如果 map 前端的节点备用空间不足,符合条件就配置一个新的map(配置更大的,拷贝原来的,释放原来的)
void reserve_map_at_front (size_type nodes_to_add = 1) {
  	if (nodes_to_add > start.node - map)
    	reallocate_map(nodes_to_add, true);
}

5. 插入和删除操作

5.1 向两端插入

void push_front(const value_type& __x)
{
    //头部buffer空间足够时,直接从后往前插入
    if (this->_M_impl._M_start._M_cur != this->_M_impl._M_start._M_first)
    {
        _Alloc_traits::construct(this->_M_impl, this->_M_impl._M_start._M_cur - 1, __x);
        --this->_M_impl._M_start._M_cur;
    }
    else
        _M_push_front_aux(__x);
}
void push_back(const value_type& t) {
    if (finish.cur != finish.last - 1) {
        construct(finish.cur, t);
        ++finish.cur;
    }
    else
        push_back_aux(t);
}
  • buffer不足,但节点足够时不重新申请节点,重新申请一个buffer即可。

  • 但如果节点不足,则重新申请一整块节点内存,并把原来的节点保存的地址都复制过去,而buffer却不会发生拷贝动作。

5.2 中间插入

从中间插入需要根据迭代器位置进行插入,调用insert函数,一个insert源代码实现如下:

template <typename _Tp, typename _Alloc>
    typename deque<_Tp, _Alloc>::iterator
    deque<_Tp, _Alloc>::
#if __cplusplus >= 201103L
    insert(const_iterator __position, const value_type& __x)
#else
    insert(iterator __position, const value_type& __x)
#endif
    {
    // 这里迭代器是头端当前迭代器就直接变为从头端插入了
    if (__position._M_cur == this->_M_impl._M_start._M_cur)
    {
         push_front(__x);
         return this->_M_impl._M_start;
     }
    // 这里迭代器是尾端当前迭代器就直接变为从尾端插入了
    else if (__position._M_cur == this->_M_impl._M_finish._M_cur)
    {
         push_back(__x);
         iterator __tmp = this->_M_impl._M_finish;
         --__tmp;
         return __tmp;
     }
    // 这里才是正常从中间插入
    else
        return _M_insert_aux(__position._M_const_cast(), __x);
}

根据待插入位置来决定是移动前半部分还是后半部分,而是否需要扩充容器大小还是由头端插入和尾端插入完成的,这里可以看出,中间插入的时间复杂度为O(n)。

5.3 从两端删除

void pop_back() _GLIBCXX_NOEXCEPT
{
    __glibcxx_requires_nonempty();
    if (this->_M_impl._M_finish._M_cur != this->_M_impl._M_finish._M_first)
    {
        --this->_M_impl._M_finish._M_cur;
        _Alloc_traits::destroy(this->_M_impl,
            this->_M_impl._M_finish._M_cur);
    }
    else
        _M_pop_back_aux();
}

deque容器的尾端迭代器中,如果当前位置不等于开始位置,则直接把当前位置向前移动一位,并把新的当前位置的元素销毁即可,也就是说尾端迭代器所指向的当前位置其实都是已经被删除了的数据,如果已经等于开始位置,则说明要换buffer了,此时就需要调用_M_pop_back_aux函数,所以我们接下来看看这个函数的实现:

template <typename _Tp, typename _Alloc>
void deque<_Tp, _Alloc>::_M_pop_back_aux()
{
     _M_deallocate_node(this->_M_impl._M_finish._M_first);
     this->_M_impl._M_finish._M_set_node(this->_M_impl._M_finish._M_node - 1);
     this->_M_impl._M_finish._M_cur = this->_M_impl._M_finish._M_last - 1;
     _Alloc_traits::destroy(_M_get_Tp_allocator(), this->_M_impl._M_finish._M_cur);
}

直接释放了当前尾端迭代器所在的buffer,然后先计算出来新的当前位置,最后才进行删除动作,根据该逻辑,尾端删除时间复杂度为O(1)。头端删除与尾端删除大同小异。

5.4 从中间删除

从中间删除会调用erase函数,deque容器有诸多erase函数的重载,我们选取其中一个进行解析,如下:

iterator
#if __cplusplus >= 201103L
    erase(const_iterator __first, const_iterator __last)
#else
    erase(iterator __first, iterator __last)
#endif
    { return _M_erase(__first._M_const_cast(), __last._M_const_cast()); }

这个函数根据两个位置删除一段数据,直接调用的_M_erase,看下这个函数的实现:

template <typename _Tp, typename _Alloc>
typename deque<_Tp, _Alloc>::
iterator deque<_Tp, _Alloc>::_M_erase(iterator __first, iterator __last)
{
    //如果开始位置等于结束位置,就不用删除了
    if (__first == __last)
    return __first;
    //如果开始位置等于容器的开始位置,结束位置等于容器的结束位置,那么直接整个容器清空即可
    else if (__first == begin() && __last == end())
    {
        clear();
        return end();
    }
    else
    {
        const difference_type __n = __last - __first;
        const difference_type __elems_before = __first - begin();
        //与从中间插入逻辑类似,如果待插入数据段前面的元素少于后面的元素数量,则从头端进行处理,否则从尾端处理
        if (static_cast<size_type>(__elems_before) <= (size() - __n) / 2)
        {
            //如果待删除数据段开始位置不等于容器开始位置,那么先把头端遗留数据向后覆盖
            if (__first != begin())
                _GLIBCXX_MOVE_BACKWARD3(begin(), __first, __last);
                //删除多余元素
                _M_erase_at_begin(begin() + __n);
            }
            else
            {
                //与if类似,这里不多说了
                if (__last != end())
                    _GLIBCXX_MOVE3(__last, end(), __first);
                    _M_erase_at_end(end() - __n);
                }
      		return begin() + __elems_before;
    	    }
    	}
    }
}

从中间删除,其实是先用头端或者尾端数据把要删除的数据覆盖掉,然后再从头端和尾端删除掉多余的数据,在这个过程中,如果待删除数据段有跨buffer,那么这个buffer也会被销毁。从中间删除元素的时间复杂度是O(n)。

6. deque成员函数

函数成员 函数功能
begin() 返回指向容器中第一个元素的迭代器。
end() 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。
rbegin() 返回指向最后一个元素的迭代器。
rend() 返回指向第一个元素所在位置前一个位置的迭代器。
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
size() 返回实际元素个数。
max_size() 返回容器所能容纳元素个数的最大值。这通常是一个很大的值,一般是 232-1,我们很少会用到这个函数。
resize() 改变实际元素的个数。
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
shrink _to_fit() 将内存减少到等于当前元素实际所使用的大小。
at() 使用经过边界检查的索引访问元素。
front() 返回第一个元素的引用。
back() 返回最后一个元素的引用。
assign() 用新元素替换原有内容。
push_back() 在序列的尾部添加一个元素。
push_front() 在序列的头部添加一个元素。
pop_back() 移除容器尾部的元素。
pop_front() 移除容器头部的元素。
insert() 在指定的位置插入一个或多个元素。
erase() 移除一个元素或一段元素。
clear() 移出所有的元素,容器大小变为 0。
swap() 交换两个容器的所有元素。
emplace() 在指定的位置直接生成一个元素。
emplace_front() 在容器头部生成一个元素。和 push_front() 的区别是,该函数直接在容器头部构造元素,省去了复制移动元素的过程。
emplace_back() 在容器尾部生成一个元素。和 push_back() 的区别是,该函数直接在容器尾部构造元素,省去了复制移动元素的过程。

 

posted @ 2024-04-22 15:30  小熊酱  阅读(1)  评论(0编辑  收藏  举报