1、簡介
雙端隊列deque,與vector的最大差異在於:
一、deque運行常數時間對頭端或尾端進行元素的插入和刪除操作。
二、deque沒有所謂的容器概念,因為它是動態地以分段連續空間組合而成隨時可以增加一塊新的內存空間並拼接起來。
雖然deque也提供隨機訪問的迭代器,但它的迭代器與list和vector的不一樣,其設計相當復雜而精妙。因此,會對各種運算產生一定影響,廚房必要,盡可能的選擇使用vetor而非deque。隊列示意圖如下圖所示:
2、deque的中控器
deque在邏輯上看起來是連續的空間,內部確實是一段一段的定量連續空間構成。
一旦有必要在deque的前端或尾端增加新空間,deque會配置一段定量的連續空間,串聯在整個deque的頭部或尾部。
deque的設計大師最大的調整應該就是如何在這段分段的定量連續空間上還能維護其整體連續的假象,並提供其隨機存取的接口,從而避開了像vector那樣的“重新配置-復制-釋放”開銷三部曲。———這樣一來,雖然開銷降低,卻提高了復雜的迭代器架構。
因此,deque數據結構的設計和迭代前進或后退等操作都非常復雜。
deque采用一塊所謂的map(注意,不是stl里面的map容器)作為中控器,其實就是一小塊連續空間,其中的每一個元素都是指針,指向另外一段較大的連續線性空間,稱之為緩沖區。在后面我們將看到,緩沖區才是deque的存儲空間主體。
#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的關系如下:
3、deque的迭代器
deque是分段連續空間,維持其“整體連續”假象的任務,就靠它的迭代器來實現,也就是operator++和operator--兩個算子上面。在開發者看來,設計deque迭代器應該具備如下三個特征的結構和功能:
一、既然deque存儲空間是分段的連續空間,迭代器應該能夠指出當前的連續空間在哪里。
二、因為緩沖區有邊界,迭代器還應該能判斷當前是否處於緩沖區的邊緣,如果是,一旦前進或后退,就必須跳轉到下一個或上一個緩沖區。
三、也就是實現前面兩種情況的前提,迭代器必須隨時控制中控器。
有了這些分析之后,在分析源碼時,就顯得容易理解了。
#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的每一個緩沖區設計了三個迭代器:
struct __deque_iterator { ... typedef T value_type; T* cur; T* first; T* last; typedef T** map_pointer; map_pointer node; ... };
為什么這么設計呢?因為deque是分段連續的空間,下圖描繪了deque的中控器、緩沖區、迭代器之間的相互關系:
map的每一段都指向一個緩沖區buffre,而緩沖區是需要知道每個元素的位置的,所以需要這些迭代器去訪問。其中:
- cur表示當前所指向的位置;
- first表示當前數組中頭的位置;
- last表示當前數組中尾的位置。
這樣設計顯然是為了方便管理,需要注意的是deque的空間由map管理的。它是一個指向指針的指針。所以三個參數都是指向當前的數組,但這樣的數組可能有多個,只是每個數組都管理這3個變量。
最后,deque緩沖區的大小由一個全局函數來決定:
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 則返回 521/sz //如果 sz 不小於 512 則返回 1
用例分析:
假設現在構造一個int類型的deque,設置緩沖區大小等於32,這樣一來,每個緩沖區可以容納32/sizeof(int)=8個元素。經過一番操作之后,deuqe現在有20個元素來,那么成員函數begin()和end()返回的兩個迭代器應該是什么樣的呢?如下圖所示:
20個元素需要20/8≈3個緩沖區。
所以map運用的三個節點,迭代器start內的cur指針指向緩沖區的第一個元素,迭代器finish內的cur指針指向緩沖區的最后一個元素(的下一個位置)。
注意:最后一個緩沖區尚有備用空間,如果之后還有新元素插入,則直接插入到備用空間。
4、迭代器的操作
迭代器操作包括兩種:前進和后退。
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; } //結合前面的分段連續空間,你在想一想這樣的設計是不是合理呢?
5、deque的構造和析構函數
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_font這兩個函數來判斷,實際操作由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); }
6、deque的插入元素和刪除元素
因為deque是能夠雙向操作,所以其push和pop操作都類似於list,都可以直接有對應的操作,需要注意的是list是鏈表,並不會涉及到界線的判斷,而deque是由數組來存儲的,所以需要隨時對界限進行判斷。
push的實現:
template <class T, class Alloc = alloc, size_t BufSiz = 0> class deque { ... public: // push_* and pop_* // 對尾進行插入 // 判斷函數是否達到了數組尾部. 沒有達到就直接進行插入 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); } ... };
pop的實現:
template <class T, class Alloc = alloc, size_t BufSiz = 0> class deque { ... public: // 對尾部進行操作 // 判斷是否達到數組的頭部. 沒有到達就直接釋放 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(); } ... };
pop和push都先調用了reserve_map_at_XX函數,這些函數主要為了判斷前后空間是否足夠。
刪除操作
構造函數都會調用create_map_and_nodes函數,考慮到deque實現前后插入時間復雜度為O(1),保證了在前后留出了空間,所以push和pop都可以在前面的數組進行操作。
現在來分析erase,因為deque是由數組構成,所以地址空間是連續的,刪除也就像vector一樣,需要移動所有的元素。
deque為了保證效率盡可能高,就判斷刪除的位置上中間偏后還是中間偏前來進行移動。
template <class T, class Alloc = alloc, size_t BufSiz = 0> class deque { ... public: // 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函數. iterator erase(iterator first, iterator last); void clear(); ... };
最后說一下insert函數。
deque源碼,基本每一個insert重載函數都會調用insert_auto判斷插入的位置離頭還是尾比較近。
如果離頭近,則先將頭往前移動,調整將要移動的距離,用copy進行調整。
如果離尾近,則將往前移動,調整將要移動的距離,用copy進行調整。
注意:
push_back則先執行構造再移動node,而push_front是先移動node再進行構造,實現的差異主要是finish是指向最后一個元素的后一個地址,而first指向的是第一個元素的地址,下面pop也是一樣的。
deque源碼里還有一些其它的成員函數:
reallocate_map:判斷中考的容量是否夠用,如果不夠用,申請更大的空間,拷貝元素過去,修改map和start,finish的指向。
fill_initialize:申請空間,對每個空間進行初始化,最后一個數組單獨處理。畢竟最后一個數組一般不會全部填滿。
clear:刪除所有的元素,分兩步執行:
首先,從第二個數組開始到倒數第二個數組一次性全部刪除,這樣做是考慮到中間的數組肯定都是滿的,前后兩個數組則不一定是滿的,最后刪除前后兩個數組元素。
deque的swap操作:只是交換了start,finish,map,並沒有交換所有的元素。
resize:重新將deque進行調整,實現方式與list一樣。
析構函數:分步釋放內存。
7、deque總結
deque實際上是在功能上合並了vector和list。
優點:
- 隨機訪問方便,即支持[]操作和vector.at();
- 在內部方便的進行插入和刪除操作;
- 可在兩端進行push、pop。
缺點:
- 因為涉及數據結構的維護比較復雜,采用分段連續空間,所以占有內存相對多。
使用區別:
- 如果需要高效的隨機存儲,而不在乎插入和刪除的效率,則使用vector。
- 如果需要大量的插入和刪除,而不關心隨機存取,則應使用list。
- 如果需要隨機存取,且關心亮度數據的插入和刪除,則應使用deque。