STL容器之deque数据结构解析

今天我们来看deque这个数据结构。

  我们在C语言的数据结构之中,应该是没有deque这个数据结构的,但是我们肯定有两个数据结构,一个叫做quene(就是所谓的队列),还有一个叫做stack(也就是所谓栈),当然对于我们来说应该是自己写出来的,但是在c++当中他们两个的实现就不一样了,他们采用同一个底层数据结构叫做deque的来进行扩充。

  deque这个结构体可以说是list和vector的一个混合结构,所以我们也不能说他是连续空间还是非连续空间。但是起码在应用层(也就是我们看到的这层),他是一个连续空间(因为可以随机跳转)。那么接下来我们就说一下这个deque在源码当中的实现。

  一、为什么要用deque

  二、Deque是什么

  三、Deque的源码实现

  四、Deque迭代器的实现

  五、deque中包含的各种函数说明和软件架构

  六、Deque的优点和缺点

  一、为什么要用deque

    在C语言中我们有很多的数据结构,比如队列,链表等等。这些大部分都在STL容器当中实现了,但是队列和栈我们始终没有实现。所以deque就出现了。

  二、Deque是什么

    Deque的用途既包含了数据结构中队列的功能,也包含了栈的功能(因为他是两端都可以插入的),当然我们说list也可以两端插入的,不过list不支持随机读取,但是Deque可以支持随机查找,但是它的插入效率要低于vector(这也好理解,因为判断的东西多了嘛)。

  三、deque的源码实现

    首先我们来看一下deque的内存模型:

   我们来分析一下deque的内存模型。首先deque中包含一个map(注意,这个map不是STL中的map),他是一个指针数组,里面存放了指针,每个指针中指向了一段内存,而这段内存才是真正存放数据的地方。所以,deque当中必须包含两个迭代器指针,分别是start和finish这两个迭代器,每个迭代器中又包含了4个指针,分别是start,finish,current以及size这四个部分。所以,一个空的deque应该是有16个指针的。

  这里说一下,对于源码来说,我们使用的时候,比如*iterator这个函数,迭代器会自动把当前数据放到cur当中,非常方便,但是这个要花点时间,因为毕竟函数重载了嘛。

  好的,接下来我们说一下deque的源码实现。deque的源码部分我们可以分成三个部分:

    一、初始化部分

    二、insert部分(包括头部添加,尾部添加,中间添加)

    三、删除部分

    一、初始化部分

      我们首先来看第一部分初始化部分

        

template<typename _Tp, typename _Alloc>

    void

    _Deque_base<_Tp, _Alloc>::

    _M_initialize_map(size_t __num_elements)

    {

      const size_t __num_nodes = (__num_elements/ __deque_buf_size(sizeof(_Tp))

                          + 1);

      this->_M_impl._M_map_size = std::max((size_t) _S_initial_map_size,

                                 size_t(__num_nodes + 2));

      this->_M_impl._M_map = _M_allocate_map(this->_M_impl._M_map_size);

      // For "small" maps (needing less than _M_map_size nodes), allocation

      // starts in the middle elements and grows outwards.  So nstart may be

      // the beginning of _M_map, but for small maps it may be as far in as

      // _M_map+3.

      _Map_pointer __nstart = (this->_M_impl._M_map

                         + (this->_M_impl._M_map_size - __num_nodes) / 2);

      _Map_pointer __nfinish = __nstart + __num_nodes;

      __try

      { _M_create_nodes(__nstart, __nfinish); }

      __catch(...)

      {

        _M_deallocate_map(this->_M_impl._M_map, this->_M_impl._M_map_size);

        this->_M_impl._M_map = _Map_pointer();

        this->_M_impl._M_map_size = 0;

        __throw_exception_again;

      }

      this->_M_impl._M_start._M_set_node(__nstart);

      this->_M_impl._M_finish._M_set_node(__nfinish - 1);

      this->_M_impl._M_start._M_cur = _M_impl._M_start._M_first;

      this->_M_impl._M_finish._M_cur = (this->_M_impl._M_finish._M_first

                              + __num_elements

                              % __deque_buf_size(sizeof(_Tp)));

    }

这里我要说明的部分有两点:

  首先是这句话:

this->_M_impl._M_map_size = std::max((size_t) _S_initial_map_size,

                                 size_t(__num_nodes + 2));

  这句话指定的是一开始map中节点的个数。我们可以看到,map所用的是初始化节点和我们指定的节点的最大值,而初始化节点大小是8,所以map中节点最少是8,或者是我们指定节点的数字加2,为什么要加2呢?因为头部和尾部各加一个,这样比较方便。

  然后这里是第二句话:

_Map_pointer __nstart = (this->_M_impl._M_map

                         + (this->_M_impl._M_map_size - __num_nodes) / 2);

  这句话指定的是头节点nstart所使用的节点数。我们注意,deque的start不是在开头,而是在中间,为什么要这么设计呢?诚如之间所说,deque本身是两头扩充的,这样做比较方便。

  接下来我们来看一下inert的部分

    我们以push_back为例:

 push_back(bool __x)

      {

  if (this->_M_impl._M_finish._M_p != this->_M_impl._M_end_addr())

    *this->_M_impl._M_finish++ = __x;

  else

    _M_insert_aux(end(), __x);

      }

这个代码比较好理解,就是如果说没到节点的尾端,那么直接插入,否则的话执行_M_insert_aux这个函数

typename deque<_Tp, _Alloc>::iterator

      deque<_Tp, _Alloc>::

      _M_insert_aux(iterator __pos, const value_type& __x)

      {

    value_type __x_copy = __x; // XXX copy

#endif

    difference_type __index = __pos - this->_M_impl._M_start;

    if (static_cast<size_type>(__index) < size() / 2)

      {

        push_front(_GLIBCXX_MOVE(front()));

        iterator __front1 = this->_M_impl._M_start;

        ++__front1;

        iterator __front2 = __front1;

        ++__front2;

        __pos = this->_M_impl._M_start + __index;

        iterator __pos1 = __pos;

        ++__pos1;

        _GLIBCXX_MOVE3(__front2, __pos1, __front1);

      }

    else

      {

        push_back(_GLIBCXX_MOVE(back()));

        iterator __back1 = this->_M_impl._M_finish;

        --__back1;

        iterator __back2 = __back1;

        --__back2;

        __pos = this->_M_impl._M_start + __index;

        _GLIBCXX_MOVE_BACKWARD3(__pos, __back2, __back1);

      }

    *__pos = _GLIBCXX_MOVE(__x_copy);

    return __pos;

  它的这个插入是很有趣的,它可以这样看,首先是对其进行赋值(拷贝一下),然后我们判断是否当前的插入位置在前半段还是后半段,这是为什么呢?我们接着往后看,如果是前半段,那么我们push_front这个函数,然后将他移动到中间。这就明白了吧,这样可以减少移动次数。

  四、Deque迭代器的使用

    我们这里以operator++举例

    

 _Self&

      operator++() _GLIBCXX_NOEXCEPT

      {

      ++_M_cur;

      if (_M_cur == _M_last)

        {

          _M_set_node(_M_node + 1);

          _M_cur = _M_first;

        }

      return *this;

      }

我们看到,如果当前节点不到last的时候M_cur正常++,反之则要跳到下一个节点。我们继续看

  

void

      _M_set_node(_Map_pointer __new_node) _GLIBCXX_NOEXCEPT

      {

      _M_node = __new_node;

      _M_first = *__new_node;

      _M_last = _M_first + difference_type(_S_buffer_size());

      }

再这里,我们跳入了下一个节点,并且重新将M_first,last重新定义,并且再返回时候将first赋值给了current

  以上就是deque的源码,今天就分享到这了。

原文地址:https://www.cnblogs.com/songyuchen/p/14357521.html