簡體   English   中英

STL 中的雙端隊列到底是什么?

[英]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_frontpush_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的核心代碼,主要是三個部分:

  1. 迭代器

  2. 如何構造deque

1. 迭代器( __deque_iterator

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

2.如何構造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 = 雙端隊列

可以向任一方向生長的容器。

雙端隊列通常實現為vectorvectors (向量列表不能提供恆定時間隨機訪問)。 雖然輔助向量的大小取決於實現,但常見的算法是使用以字節為單位的恆定大小。

(這是我在另一個線程中給出的答案。本質上,我認為即使是相當幼稚的實現,使用單個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*的操作,因此無關緊要。 如果該標准指的是復雜性的“傳統”理論概念,那么他們就不會明確地將自己限制為“對所包含對象的操作數量”。 我是否過度解釋了這句話?

我正在閱讀 Adam Drozdek 的“C++ 中的數據結構和算法”,發現這很有用。 HTH。

STL deque 的一個非常有趣的方面是它的實現。 STL 雙端隊列不是作為鏈表實現的,而是作為指向塊或 arrays 數據的指針數組實現的。 塊的數量根據存儲需求動態變化,指針數組的大小也會相應變化。

您可以注意到中間是指向數據的指針數組(右側的塊),並且您還可以注意到中間的數組是動態變化的。

一張圖片勝過千言萬語。

在此處輸入圖像描述

雖然該標准不要求任何特定的實現(僅是恆定時間隨機訪問),但通常將雙端隊列實現為連續 memory “頁面”的集合。 根據需要分配新頁面,但您仍然可以隨機訪問。 std::vector不同,您不能保證數據是連續存儲的,但就像向量一樣,中間的插入需要大量的重定位。

deque可以實現為固定大小數組的循環緩沖區:

  • 使用循環緩沖區,因此我們可以通過添加/刪除具有 O(1) 復雜度的固定大小的數組來在兩端增長/縮小
  • 使用固定大小的數組,因此很容易計算索引,因此通過索引訪問兩個指針取消引用 - 也是 O(1)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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