[英]What data structure, exactly, are deques in C++?
是否有一個特定的數據結構,C ++ STL中的deque應該實現,或者是一個deque只是這個模糊的概念,一個陣列可以從前面和后面增長,然而實現選擇實現?
我以前總是假設deque是一個循環緩沖區 ,但我最近在這里讀了一個C ++引用,聽起來像deque是某種數組的數組。 它似乎不是一個普通的舊循環緩沖區。 那么它是一個間隙緩沖區 ,還是可擴展數組的其他變體 ,還是僅僅依賴於實現?
答案的更新和摘要 :
似乎普遍的共識是,雙端隊列是一種數據結構,這樣:
如果我們將第一個條件設為“非攤銷的恆定時間”,似乎沒有人知道如何得到第一和第四條件的組合。 鏈表實現1)但不是4),而典型的循環緩沖實現4)但不實現1)。 我想我的實現可以滿足以下兩個要求。 評論?
我們從其他人建議的實現開始:我們分配一個數組並從中間開始放置元素,在前面和后面留下空間。 在這個實現中,我們跟蹤中心在前后方向上有多少元素,調用那些值F和B.然后,讓我們用一個兩倍於原始大小的輔助數組來擴充這個數據結構。數組(所以現在我們浪費了大量的空間,但漸近的復雜性沒有變化)。 我們還將從中間填充這個輔助數組,並給它類似的值F'和B'。 策略是這樣的:每次我們在給定方向上向主數組添加一個元素時,如果F> F'或B> B'(取決於方向),最多兩個值從主數組復制到輔助數據數組直到F'趕上F(或B'跟B)。 因此,插入操作涉及將1個元素放入主數組並從主數據庫復制到輔助數據2,但它仍然是O(1)。 當主陣列變滿時,我們釋放主陣列,使輔助陣列成為主陣列,並制作另一個大2倍的輔助陣列。 這個新的輔助數組以F'= B'= 0開始,並且沒有復制到它(因此如果堆分配是O(1)復雜度,則調整大小op為O(1))。 由於添加到主要和主要的每個元素的輔助副本2個元素最多開始半滿,因此當主要用完空間時,輔助節點不可能趕上主要元素。 刪除同樣只需要從主要刪除1個元素,從輔助刪除0或1。 因此,假設堆分配為O(1),則此實現滿足條件1)。 我們使數組為T *並在插入時使用new
以滿足條件2)和3)。 最后,4)被實現,因為我們使用數組結構並且可以輕松實現O(1)訪問。
它是特定於實現的。 所有deque要求是在開始/結束時的恆定時間插入/刪除,並且在其他地方最多是線性的。 元素不需要是連續的。
大多數實現使用可以描述為展開列表的內容。 固定大小的數組在堆上分配,指向這些數組的指針存儲在屬於deque的動態大小的數組中。
deque通常被實現為T
的數組的動態陣列。
(a) (b) (c) (d)
+-+ +-+ +-+ +-+
| | | | | | | |
+-+ +-+ +-+ +-+
^ ^ ^ ^
| | | |
+---+---+---+---+
| 1 | 8 | 8 | 3 | (reference)
+---+---+---+---+
陣列(a),(b),(c)和(d)通常具有固定容量,內部陣列(b)和(c)必須是滿的。 (a)和(d)未滿,兩端插入O(1)。
想象我們做了很多push_front
,(a)將填滿,當它已滿並且執行插入時我們首先需要分配一個新數組,然后增長(引用)向量並將指針推送到新數組面前。
這個實現簡單地提供:
min(distance(begin, it), distance(it, end))
成比例(標准比你要求的更嚴格) 但是,它沒有按攤銷O(1)增長的要求。 因為每當(引用)向量需要增長時,數組都具有固定容量,所以我們有O(N /容量)指針副本。 因為指針被輕易復制,所以可以進行單個memcpy
調用,因此在實踐中這通常是不變的...但這不足以通過飛行顏色。
仍然, push_front
和push_back
比vector
更有效(除非你使用MSVC實現,因為數組的容量非常小,因此速度非常慢......)
老實說,我知道沒有數據結構或數據結構組合可以滿足兩者:
和
我知道幾個“近”的比賽:
deque
的“參考保留”語義不兼容 可以通過使用vector<T*>
正確地實現deque<T>
vector<T*>
。 所有元素都復制到堆上,指針存儲在向量中。 (稍后有關矢量的更多信息)。
為什么T*
而不是T
? 因為標准要求
“在雙端隊列的任一端插入都會使deque的所有迭代器無效,但對deque元素的引用的有效性沒有影響。 ”
(我的重點)。 T*
有助於滿足這一要求。 它也有助於我們滿足這一要求:
“在雙端隊列的開頭或末尾插入單個元素總是.....導致對T的構造函數的單個調用 。”
現在為(有爭議的)位。 為什么使用vector
來存儲T*
? 它為我們提供隨機訪問,這是一個良好的開端。 讓我們暫時忘記向量的復雜性並仔細構建:
該標准談到“所包含對象的操作次數”。 對於deque::push_front
這顯然是1,因為只構造了一個T
對象,並且以任何方式讀取或掃描現有T
對象的零。 這個數字1顯然是一個常數,與目前在雙端隊列中的對象數量無關。 這讓我們可以這樣說:
'對於我們的deque::push_front
,包含對象上的操作數(Ts)是固定的,並且與雙端隊列中已有的對象數無關。
當然, T*
上的操作次數不會那么好。 當vector<T*>
變得太大時,它將被重新分配並且將復制許多T*
s。 所以是的, T*
上的操作數量會有很大差異,但T
上的操作數量不會受到影響。
為什么我們關心T
計數操作和T*
上的計數操作之間的這種區別? 這是因為標准說:
本節中的所有復雜性要求僅根據所包含對象的操作數量來說明。
對於deque
,包含的對象是T
,而不是T*
,這意味着我們可以忽略任何復制(或重新分配) T*
。
我沒有多說過一個向量在雙端隊列中的表現。 也許我們會把它解釋為一個循環緩沖區(向量總是占用它的最大capacity()
,然后當向量已滿時將所有內容重新分配到一個更大的緩沖區。細節無關緊要。
在最后幾段中,我們分析了deque::push_front
以及deque::push_front
中對象數量與push_front對包含的T
-objects執行的操作數之間的關系。 我們發現它們彼此獨立。 由於標准要求復雜性就T
操作而言,我們可以說這具有不變的復雜性。
是的, 運營-ON-T * -復雜攤銷(由於vector
),但我們只在操作-ON-感興趣T
-復雜性 ,這是不變的(非攤銷)。
結語:vector :: push_back或vector :: push_front的復雜性與此實現無關; 這些考慮涉及對T*
操作,因此無關緊要。
(讓這個答案成為社區維基。請陷入困境。)
首先要做的事情是: deque
要求前面或后面的任何插入都應保持對成員元素的任何引用有效。 迭代器無效是可以的,但成員本身必須保持在內存中的相同位置。 只需將成員復制到堆上的某個位置並將T*
存儲在引擎蓋下的數據結構中即可。 查看其他StackOverflow問題“ 關於deque <T>的額外間接 ”
( vector
不保證保留迭代器或引用,而list
保留兩者)。
所以,讓我們把這個“間接”視為理所當然,並看看問題的其余部分。 有趣的是從列表的開頭或結尾插入或刪除的時間。 起初,看起來像deque
可以通過vector
實現,通常可以將其解釋為循環緩沖區 。
但是雙端隊列必須滿足“在雙端隊列的開頭或末尾插入單個元素總是占用恆定時間並導致對T的構造函數的單個調用。”
由於我們已經提到的間接性,很容易確保只有一個構造函數調用,但挑戰是保證恆定的時間。 如果我們可以使用常量的攤銷時間,這將允許簡單的vector
實現,但它必須是恆定的(非攤銷的)時間。
我對deque的理解
它從堆中分配'n'個空連續對象作為第一個子數組。 其中的對象在插入時由頭指針添加一次。
當頭指針到達數組的末尾時,它會分配/鏈接一個新的非連續子數組並在那里添加對象。
它們在提取時被尾指針刪除一次。 當尾指針完成對象的子數組時,它會移動到下一個鏈接的子數組,並釋放舊的數組。
頭部和尾部之間的中間對象永遠不會被deque在內存中移動。
隨機訪問首先確定哪個子數組具有該對象,然后從其在子數組中的相對偏移量訪問它。
這是對用戶引力評論2陣列解決方案的挑戰的答案。
細節討論:用戶“引力”已經給出了非常簡潔的總結。 “引力”也挑戰我們評論平衡兩個數組之間的元素數量的建議,以實現O(1)最壞情況(而不是平均情況)運行時。 好吧,如果兩個陣列都是環形緩沖區,那么解決方案可以有效地工作,而且在我看來,將雙端隊列分成兩個段就足夠了,按照建議進行平衡。 我還認為,出於實際目的,標准STL實現至少足夠好,但是在實時要求下並且通過適當調整的內存管理,可以考慮使用這種平衡技術。 Eric Demaine在一篇較舊的Dr.Dobbs文章中給出了不同的實現,具有類似的最壞情況運行時。
平衡兩個緩沖區的負載需要在0或3個元素之間移動,具體取決於具體情況。 例如,如果我們將前段保留在主陣列中,則pushFront(x)必須將最后3個元素從主環移動到輔助環以保持所需的平衡。 后部的pushBack(x)必須掌握負載差異,然后決定何時將一個元素從主陣列移動到輔助陣列。
建議改進:如果前后都存儲在輔助環中,則工作和簿記要少。 這可以通過將deque切割成三個區段q1,q2,q3來實現,這三個區段以下列方式排列:前部q1位於輔助環(雙倍大小的一個)中,並且可以從元素所在的任何偏移處開始。按順序順序排列。 q1中的元素數正好是存儲在輔助環中的所有元素的一半。 后部q3也位於輔助環中,與輔助環中的部分q1 正好相對 ,也是后續順序的順時針方向。 必須在所有雙端運算之間保持這種不變量。 只有中間部分q2位於主環中(后續順時針方向)。
現在,每個操作都會移動一個元素,或者當任何一個元素變空時分配一個新的空的ringbuffer。 例如,pushFront(x)在輔助環中將q1之前的x存儲起來。 為了保持不變量,我們將最后一個元素從q2移動到后面q3的前面。 因此,q1和q3都在其前沿獲得了額外的元素,因此彼此保持相反。 PopFront()以相反的方式工作,后面的操作以相同的方式工作。 當q1和q3彼此接觸並在輔助環內形成后續元素的整圓時,主環(與中間部分q2相同)完全變空。 此外,當deque縮小時,當q2在主環中形成適當的圓時,q1,q3將完全變空。
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
的主要功能:
首先,跳到任何塊:
void set_node(map_pointer new_node){
node = new_node;
first = *new_node;
last = first + chunk_size();
}
注意,計算塊大小的chunk_size()
函數,你可以想到它返回8以簡化這里。
operator*
獲取塊中的數據
reference operator*()const{
return *cur;
}
operator++, --
//前綴增量形式
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;
}
deque
簡單功能 deque
共同功能
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];
}
如果你想更深入地了解deque
你也可以看到這個問題https://stackoverflow.com/a/50959796/6329006
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.