簡體   English   中英

究竟什么數據結構是C ++中的deques?

[英]What data structure, exactly, are deques in C++?

是否有一個特定的數據結構,C ++ STL中的deque應該實現,或者是一個deque只是這個模糊的概念,一個陣列可以從前面和后面增長,然而實現選擇實現?

我以前總是假設deque是一個循環緩沖區 ,但我最近在這里讀了一個C ++引用,聽起來像deque是某種數組的數組。 它似乎不是一個普通的舊循環緩沖區。 那么它是一個間隙緩沖區 ,還是可擴展數組的其他變體 ,還是僅僅依賴於實現?

答案的更新和摘要

似乎普遍的共識是,雙端隊列是一種數據結構,這樣:

  • 插入或刪除元素的時間應該在列表的開頭或結尾處是恆定的,並且在其他地方最多是線性的。 如果我們將此解釋為真正的恆定時間而非攤銷的恆定時間,正如有人評論,這似乎具有挑戰性。 有些人認為我們不應將此解釋為非攤銷的常數時間。
  • “deque要求任何插入都應保持對成員元素的任何引用有效。迭代器可以無效,但成員本身必須保留在內存中的相同位置。” 正如有人評論的那樣:只需將成員復制到堆上的某個位置並將T *存儲在引擎蓋下的數據結構中即可。
  • “在雙端隊列的開頭或末尾插入單個元素總是需要一個恆定的時間,並導致對T的構造函數的單個調用。” 如果數據結構在引擎蓋下存儲T *,也將實現T的單個構造函數。
  • 數據結構必須具有隨機訪問權限。

如果我們將第一個條件設為“非攤銷的恆定時間”,似乎沒有人知道如何得到第一和第四條件的組合。 鏈表實現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_frontpush_backvector更有效(除非你使用MSVC實現,因為數組的容量非常小,因此速度非常慢......)


老實說,我知道沒有數據結構或數據結構組合可以滿足兩者:

  • 隨機訪問

  • O(1)兩端插入

我知道幾個“近”的比賽:

  • 分攤O(1)插入可以使用動態數組完成,在中間寫入,這與deque的“參考保留”語義不兼容
  • B + Tree可以通過索引而不是按鍵來提供訪問,時間接近常量,但訪問和插入的復雜度為O(log N)(使用小常量),它需要使用Fenwick樹中級節點。
  • 手指樹可以類似地調整,但它再次真的是O(log N)。

可以通過使用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內部結構

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的核心代碼,主要分為兩部分:

  1. 迭代器

  2. 關於deque簡單功能

迭代器( __deque_iterator

迭代器的主要問題是,當++, - 迭代器時,它可能跳到其他塊(如果它指向塊的邊緣)。 例如,有三個數據塊: chunk 1chunk 2chunk 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;
}

2.關於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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM