[英]What really is a deque in STL?
我正在查看 STL 容器並試圖弄清楚它們到底是什么(即使用的數據結構),但雙端隊列阻止了我:我起初以為它是一個雙鏈表,它允許從兩端插入和刪除恆定時間,但我很困擾運營商[]制作的promise要在恆定時間內完成。 在鏈表中,任意訪問應該是 O(n),對吧?
如果它是一個動態數組,它怎么能在恆定時間內添加元素呢? 應該提到的是,可能會發生重新分配,並且 O(1) 是攤銷成本,就像 vector 一樣。
所以我想知道這個結構是什么,它允許在恆定時間內任意訪問,同時永遠不需要移動到一個新的更大的地方。
雙端隊列在某種程度上是遞歸定義的:在內部它維護一個固定大小的塊的雙端隊列。 每個塊都是一個向量,塊本身的隊列(下圖中的“映射”)也是一個向量。
在CodeProject對性能特征以及它與vector
的比較進行了很好的分析。
GCC 標准庫實現在內部使用T**
來表示 map。 每個數據塊都是一個T*
,它分配有一些固定大小__deque_buf_size
(取決於sizeof(T)
)。
把它想象成一個向量的向量。 只有它們不是標准的std::vector
。
外部向量包含指向內部向量的指針。 當它的容量通過重新分配改變時,而不是像std::vector
那樣將所有空白空間分配到末尾,它在向量的開頭和結尾將空白空間分成相等的部分。 這允許此向量上的push_front
和push_back
都在攤銷的 O(1) 時間內發生。
內部向量行為需要根據它是在deque
的前面還是后面而改變。 在后面,它可以表現為標准的std::vector
,它在最后增長,並且push_back
發生在 O(1) 時間內。 在前面它需要做相反的事情,在每個push_front
開始時增長。 在實踐中,這很容易通過添加指向前端元素的指針和增長方向以及大小來實現。 通過這個簡單的修改push_front
也可以是 O(1) 時間。
訪問任何元素都需要偏移和划分到出現在 O(1) 中的適當的外部向量索引,並索引到也是 O(1) 的內部向量。 這假設內部向量都是固定大小的,除了deque
開頭或結尾的向量。
從概述中,您可以將deque
視為double-ended queue
deque
中的數據由固定大小的向量塊存儲,它們是
由map
(這也是一個向量塊,但它的大小可能會改變)
deque iterator
的主要部分代碼如下:
/*
buff_size is the length of the chunk
*/
template <class T, size_t buff_size>
struct __deque_iterator{
typedef __deque_iterator<T, buff_size> iterator;
typedef T** map_pointer;
// pointer to the chunk
T* cur;
T* first; // the begin of the chunk
T* last; // the end of the chunk
//because the pointer may skip to other chunk
//so this pointer to the map
map_pointer node; // pointer to the map
}
deque
的主要部分代碼如下:
/*
buff_size is the length of the chunk
*/
template<typename T, size_t buff_size = 0>
class deque{
public:
typedef T value_type;
typedef T& reference;
typedef T* pointer;
typedef __deque_iterator<T, buff_size> iterator;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
protected:
typedef pointer* map_pointer;
// allocate memory for the chunk
typedef allocator<value_type> dataAllocator;
// allocate memory for map
typedef allocator<pointer> mapAllocator;
private:
//data members
iterator start;
iterator finish;
map_pointer map;
size_type map_size;
}
下面我給大家介紹一下deque
的核心代碼,主要是三個部分:
迭代器
如何構造deque
__deque_iterator
) 迭代器的主要問題是,當++,--迭代器時,它可能會跳到其他塊(如果它指向塊的邊緣)。 例如,有三個數據塊: chunk 1
、 chunk 2
、 chunk 3
。
pointer1
指向chunk 2
的開頭,當操作符--pointer
時,它將指向chunk 1
的結尾,從而指向指針pointer2
。
下面我將給出 __deque_iterator 的主要__deque_iterator
:
首先,跳到任何塊:
void set_node(map_pointer new_node){
node = new_node;
first = *new_node;
last = first + chunk_size();
}
請注意,計算塊大小的chunk_size()
function ,你可以認為它返回 8 來簡化這里。
operator*
獲取塊中的數據
reference operator*()const{
return *cur;
}
operator++, --
// 增量的前綴 forms
self& operator++(){
++cur;
if (cur == last){ //if it reach the end of the chunk
set_node(node + 1);//skip to the next chunk
cur = first;
}
return *this;
}
// postfix forms of increment
self operator++(int){
self tmp = *this;
++*this;//invoke prefix ++
return tmp;
}
self& operator--(){
if(cur == first){ // if it pointer to the begin of the chunk
set_node(node - 1);//skip to the prev chunk
cur = last;
}
--cur;
return *this;
}
self operator--(int){
self tmp = *this;
--*this;
return tmp;
}
迭代器跳過 n 步/隨機訪問
self& operator+=(difference_type n){ // n can be postive or negative difference_type offset = n + (cur - first); if(offset >=0 && offset < difference_type(buffer_size())){ // in the same chunk cur += n; }else{//not in the same chunk difference_type node_offset; if (offset > 0){ node_offset = offset / difference_type(chunk_size()); }else{ node_offset = -((-offset - 1) / difference_type(chunk_size())) - 1; } // skip to the new chunk set_node(node + node_offset); // set new cur cur = first + (offset - node_offset * chunk_size()); } return *this; } // skip n steps self operator+(difference_type n)const{ self tmp = *this; return tmp+= n; //reuse operator += } self& operator-=(difference_type n){ return *this += -n; //reuse operator += } self operator-(difference_type n)const{ self tmp = *this; return tmp -= n; //reuse operator += } // random access (iterator can skip n steps) // invoke operator +,operator * reference operator[](difference_type n)const{ return *(*this + n); }
deque
deque
的常見 function
iterator begin(){return start;} iterator end(){return finish;} reference front(){ //invoke __deque_iterator operator* // return start's member *cur return *start; } reference back(){ // cna't use *finish iterator tmp = finish; --tmp; return *tmp; //return finish's *cur } reference operator[](size_type n){ //random access, use __deque_iterator operator[] return start[n]; } template<typename T, size_t buff_size> deque<T, buff_size>::deque(size_t n, const value_type& value){ fill_initialize(n, value); } template<typename T, size_t buff_size> void deque<T, buff_size>::fill_initialize(size_t n, const value_type& value){ // allocate memory for map and chunk // initialize pointer create_map_and_nodes(n); // initialize value for the chunks for (map_pointer cur = start.node; cur < finish.node; ++cur) { initialized_fill_n(*cur, chunk_size(), value); } // the end chunk may have space node, which don't need have initialize value initialized_fill_n(finish.first, finish.cur - finish.first, value); } template<typename T, size_t buff_size> void deque<T, buff_size>::create_map_and_nodes(size_t num_elements){ // the needed map node = (elements nums / chunk length) + 1 size_type num_nodes = num_elements / chunk_size() + 1; // map node num。min num is 8 ,max num is "needed size + 2" map_size = std::max(8, num_nodes + 2); // allocate map array map = mapAllocator::allocate(map_size); // tmp_start,tmp_finish poniters to the center range of map map_pointer tmp_start = map + (map_size - num_nodes) / 2; map_pointer tmp_finish = tmp_start + num_nodes - 1; // allocate memory for the chunk pointered by map node for (map_pointer cur = tmp_start; cur <= tmp_finish; ++cur) { *cur = dataAllocator::allocate(chunk_size()); } // set start and end iterator start.set_node(tmp_start); start.cur = start.first; finish.set_node(tmp_finish); finish.cur = finish.first + num_elements % chunk_size(); }
假設i_deque
有 20 個 int 元素0~19
,其塊大小為 8,現在 push_back 3 個元素 (0, 1, 2) 到i_deque
:
i_deque.push_back(0); i_deque.push_back(1); i_deque.push_back(2);
它的內部結構如下:
然后再次 push_back,它會調用 allocate new chunk:
push_back(3)
如果我們push_front
,它將在 prev start
之前分配新的塊
注意當push_back
元素放入deque 時,如果所有的maps 和chunk 都被填滿,會導致分配新的map,並調整chunk。但是上面的代碼可能足以讓你理解deque
。
deque = 雙端隊列
可以向任一方向生長的容器。
雙端隊列通常實現為vector
的vectors
(向量列表不能提供恆定時間隨機訪問)。 雖然輔助向量的大小取決於實現,但常見的算法是使用以字節為單位的恆定大小。
(這是我在另一個線程中給出的答案。本質上,我認為即使是相當幼稚的實現,使用單個vector
,也符合“恆定非攤銷 push_{front,back}”的要求。你可能是感到驚訝,並認為這是不可能的,但我在標准中發現了其他相關的引用,這些引用以一種令人驚訝的方式定義了上下文。請多多包涵;如果我在這個答案中犯了錯誤,那么確定哪個是非常有幫助的我說得對的事情以及我的邏輯已經崩潰的地方。)
在這個答案中,我不是想確定一個好的實現,我只是想幫助我們解釋 C++ 標准中的復雜性要求。 我引用了N3242 ,根據Wikipedia ,這是最新的免費提供的 C++11 標准化文檔。 (它的組織方式似乎與最終標准不同,因此我不會引用確切的頁碼。當然,這些規則可能在最終標准中發生了變化,但我認為沒有發生這種情況。)
deque<T>
可以通過使用vector<T*>
正確實現。 所有元素都復制到堆上,指針存儲在向量中。 (稍后將詳細介紹矢量)。
為什么是T*
而不是T
? 因為標准要求
“在雙端隊列的任何一端插入都會使雙端隊列的所有迭代器無效,但對雙端隊列元素引用的有效性沒有影響。 ”
(我的重點)。 T*
有助於滿足這一點。 它還可以幫助我們滿足這一點:
“在雙端隊列的開頭或結尾插入單個元素總是......會導致對 T 的構造函數的一次調用。”
現在是(有爭議的)位。 為什么使用vector
來存儲T*
? 它為我們提供了隨機訪問,這是一個好的開始。 讓我們暫時忘記向量的復雜性,並仔細構建:
該標准談到“對包含對象的操作次數”。 對於deque::push_front
這顯然是 1,因為恰好構造了一個T
object 並且以任何方式讀取或掃描了零個現有T
對象。 這個數字 1 顯然是一個常數,並且與當前在雙端隊列中的對象數量無關。 這讓我們可以說:
“對於我們的deque::push_front
,對包含的對象(Ts)的操作數量是固定的,並且與雙端隊列中已經存在的對象數量無關。”
當然, T*
上的操作次數不會那么乖。 當vector<T*>
變得太大時,它將被重新分配並且許多T*
將被復制。 所以是的, T*
上的操作數會有很大差異,但T
上的操作數不會受到影響。
為什么我們關心T
上的計數操作和T*
上的計數操作之間的這種區別? 這是因為標准說:
本節中的所有復雜性要求僅根據對包含對象的操作數量進行說明。
對於deque
,包含的對象是T
,而不是T*
,這意味着我們可以忽略任何復制(或重新分配) T*
的操作。
關於向量在雙端隊列中的行為,我沒有說太多。 也許我們會將其解釋為循環緩沖區(向量總是占用其最大capacity()
,然后當向量已滿時將所有內容重新分配到更大的緩沖區中。細節無關緊要。
在最后幾段中,我們分析了deque::push_front
以及已在 deque 中的對象數量與 push_front 對包含的T
對象執行的操作數量之間的關系。 我們發現它們是相互獨立的。 由於標准規定復雜性是基於T
上的操作,那么我們可以說這具有恆定的復雜性。
是的, Operations-On-T*-Complexity是攤銷的(由於vector
),但我們只對Operations-On-T-Complexity感興趣,它是恆定的(非攤銷的)。
vector::push_back 或 vector::push_front 的復雜性在此實現中無關緊要; 這些考慮涉及對T*
的操作,因此無關緊要。 如果該標准指的是復雜性的“傳統”理論概念,那么他們就不會明確地將自己限制為“對所包含對象的操作數量”。 我是否過度解釋了這句話?
雖然該標准不要求任何特定的實現(僅是恆定時間隨機訪問),但通常將雙端隊列實現為連續 memory “頁面”的集合。 根據需要分配新頁面,但您仍然可以隨機訪問。 與std::vector
不同,您不能保證數據是連續存儲的,但就像向量一樣,中間的插入需要大量的重定位。
deque
可以實現為固定大小數組的循環緩沖區:
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.