雙端隊列 (deque)

除了我們常見的的 vectorlist 之外,還有一種序列式容器 deque。它是一種雙向開口連續線性空間,雙向開口意味着你可以在它的頭部,尾部任意插入元素,連續線性意味着它底層如同 vector 那樣是“連續”的空間。

概述

我們知道 vector 底層是一塊連續的空間, 正因爲如此它的迭代器及其簡單,僅僅是一個 T* 類型的指針,它的缺點也顯而易見——頭插數據的效率不高,空間“增長”的花費也很昂貴(申請新空間-搬移數據-釋放舊空間)。而 deuqe 特殊的空間構造則巧妙的避免的 vector 的頭插的缺點。
實際上,它的底層是一段一段的定量的連續空間(我們可以稱這些一段一段的連續空間爲數據緩衝區),而這些數據緩衝區則由一個被稱爲中控器的結構和兩個迭代器來維護。

結構

先通過一幅圖來看看它們之間關係:
這裏寫圖片描述

它們之間繁瑣的關係讓人分析起來無從下手,但其實只要稍稍一整理,我們就可以的出:

  1. 整塊空間由一個T** 的指針( map )和兩個迭代器維護( startfinish )。
  2. map 指向一段定長的連續線性空間,這些空間中儲存的是一個個指向“數據緩衝區”的指針。(map如同一個指針數組)
  3. start 迭代器指的 cur 向第一個緩衝區的第一個元素, finish 迭代器的 cur 指向最後一個緩衝區的最後一個元素的下一個元素。即: start “指向”“整塊空間”的第一個元素, finish “指向”“整塊空間”的最後一個元素。
  4. 數據實際上是儲存在數據緩衝區中,而中控器只是負責把多個緩衝區“鏈接起來”, 從而造成“整塊空間連續的假象”, 而 startfinish 則標記着有效數據的空間的頭和尾。

通過上面一系列的分析,我們知道, deuqe 所謂的“連續的空間”只是一種假象,實際上它是通過中控器將多段連續空間拼接在一起。
下面是 STL 庫中的相關源碼(STL3.0):

...
template <class T, class Alloc = alloc, size_t BufSiz = 0> 
class deque {
public:                         // Basic types
  typedef T value_type;
  typedef value_type* pointer;
...
  typedef pointer* map_pointer;
...
protected:                      // Data members
  iterator start;
  iterator finish;

  map_pointer map;
  size_type map_size;

源碼中除了管理我們前面所說的一個指針兩個迭代器之外,還有一個標記map大小的變量。

迭代器

通過以上的學習,我們知道 deque 的結構與 vector 相比頗爲複雜,同樣的,它的迭代器結構也較複雜。
下面是迭代器相關的源碼:

...

  T* cur;
  T* first;
  T* last;
  map_pointer node;
//迭代器的構造函數
  __deque_iterator(T* x, map_pointer y) 
    : cur(x), first(*y), last(*y + buffer_size()), node(y) {}
  __deque_iterator() : cur(0), first(0), last(0), node(0) {}
  __deque_iterator(const iterator& x)
    : cur(x.cur), first(x.first), last(x.last), node(x.node) {}

...

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

通過代碼,我們很容易參透該迭代器的奧祕:

map : 當前迭代器標識的是哪個緩衝區;
first : 指向當前緩衝區的頭部;
last:指向當前緩衝區的尾部;
cur:指向當前緩衝區的某個具體數據。

該迭代器通過上面 cur 成員就可以精準的定位到該結構中的具體某個數據,不但如此, startfinishnode 之間的相互配合得以讓該迭代器在不同數據緩衝區之間跳躍,這一特性將使得迭代器的自增、自減操作簡單許多。

下面是它的自增、自減源碼:

...

  self& operator++() {
    ++cur;
    if (cur == last) {
      set_node(node + 1);
      cur = first;
    }
    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;
  }

...
//該函數讓當前迭代器跳到一個新的緩衝區(new_node)上
  void set_node(map_pointer new_node) {
    node = new_node;
    first = *new_node;
    last = first + difference_type(buffer_size());
  }
...

顯然,在進行自增操作後,如果 cur 已經到了結尾, 則讓迭代器跳到下一個緩衝區上,並且讓 cur 指向下一個緩衝區的頭部(符合左閉右開原則)。同樣的,在自減前,如果 cur 指向當前緩衝區的頭部,則先跳到前一個緩衝區上,再讓 cur 指向前一個緩衝區的尾部(仍符合左閉右開)。
下面的圖將有助於理解自減這一過程:

這裏寫圖片描述
自增操作過程和上圖如出一轍。

數據操作

  • push_back()
    deque 的尾插操作和 vector 極爲相似,不同的是擴容處理。我們知道當 vector 沒有預留空間時,會重新找一塊更大的空間,然後拷貝數據,釋放原空間,而 deque 卻不同。它是怎樣做的呢?如下:

先插入三個數據:

push_back(1);
push_back(2);
push_back(3);

這裏寫圖片描述

再插入四個個數據:

push_back(4);
push_back(5);
push_back(6);
push_back(7);

此時它的數據情況如下圖左側的,然後再插入一個數據後, finish 迭代器將發生變化,如下:
這裏寫圖片描述
可以看出當把第一個緩衝區插滿時,此時,先申請一塊新的緩衝區,然後讓當前 node 的下一個節點指向它,再調整 finish 迭代器。現在 finish 迭代器指向的是下一個緩衝區的起始位置,這也符合我們所說的,finish 指向最後一個元素的下一個位置,從表面來看,也使得 deque 如同“一段連續的空間”。

以上就是數據緩衝區的擴容,在有些情況下,中控器也需要擴容。中控器的擴容類似於 vector 的擴容,當中控器中的所有節點都已被分配緩衝區時,且邊界的緩衝區已滿的情況下再插入數據時,就需要給中控器擴容。

它的擴容做法如下:
這裏寫圖片描述

  • push_front()
    頭插數據和尾插數據如出一轍。這裏不再詳述。

  • pop_back()、 pop_front()
    刪除數據再簡單不過了。頭刪數據的做法是讓 start 迭代器的 cur 後移一個位置,若已經到了緩衝區結尾,則釋放該緩衝區,並且讓 start 跳到下一個緩衝區,cur 指向其起始位置。如果刪除後整個 deque 已經沒有數據,則釋放掉 map
    尾刪也是如此,只不過是 cur 向前走。

下面給出在頭部和尾部插入和刪除數據的部分源代碼(更多詳細源碼可至STL源碼下載網站下載,鑑於3.0之後的版本源碼命名風格,其閱讀成本稍大,建議下載3.0之前的,僅供學習):

  void push_back(const value_type& t) {
    //如果沒到緩衝區尾部,則世界插入數據
    if (finish.cur != finish.last - 1) {
      construct(finish.cur, t);
      ++finish.cur;
    }
    //否則調用備用插入函數進行插入
    else
      push_back_aux(t);//
  }

  void push_front(const value_type& t) {
    if (start.cur != start.first) {
      construct(start.cur - 1, t);
      --start.cur;
    }
    else
      push_front_aux(t);
  }

  void pop_back() {
    //如果被刪除數據不是緩衝區的第一個數據,則直接刪除
    if (finish.cur != finish.first) {
      --finish.cur;
      destroy(finish.cur);
    }
    //否則調用備用刪除方法
    else
      pop_back_aux();
  }

  void pop_front() {
    if (start.cur != start.last - 1) {
      destroy(start.cur);
      ++start.cur;
    }
    else 
      pop_front_aux();
  }
  ...
  //以下是備用的增刪函數
  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();
  //給下一個緩衝區分配空間
  *(finish.node + 1) = allocate_node();
  __STL_TRY {
    //插入數據
    construct(finish.cur, t_copy);
    //將 `finish` 調整到下個緩衝區起始位置
    finish.set_node(finish.node + 1);
    finish.cur = finish.first;
  }
  __STL_UNWIND(deallocate_node(*(finish.node + 1)));
}

// Called only if 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`使其指向上個緩衝區的最後一個數據
  finish.set_node(finish.node - 1);
  finish.cur = finish.last - 1;
  //析構該數據
  destroy(finish.cur);
}

...

deque 複雜的結構和迭代器設計使得我們在平常很少使用它,而相對簡單的 vector 則被頻繁的使用,當然這個要視場景而定。
實際上,stackqueue 默認使用的容器就是 deque

這裏寫圖片描述


—— 謝謝!


參考資料

【作者:果凍 http://blog.csdn.net/jelly_9

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章