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的源碼,今天就分享到這了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM