什么是STL的双端队列?

时间:2011-06-09 11:52:48

标签: c++ stl deque

我正在查看STL容器并试图弄清楚它们到底是什么(即使用的数据结构), deque 阻止了我:我起初认为它是双链表,这将允许在恒定时间内从两端插入和删除,但是the promise made让操作符[]在恒定时间内完成。在链表中,任意访问应该是O(n),对吗?

如果它是一个动态数组,那么add elements如何在恒定的时间内?应该提到的是,可能会发生重新分配,并且O(1)是摊销成本,like for a vector

所以我想知道这个结构是什么允许在恒定时间内任意访问,同时永远不需要移动到一个新的更大的地方。

8 个答案:

答案 0 :(得分:150)

deque在某种程度上是递归定义的:在内部它维护一个固定大小的的双端队列。每个块都是一个向量,块本身的队列(下图中的“map”)也是一个向量。

schematic of the memory layout of a deque

对性能特征进行了很好的分析,并与CodeProject上的vector进行了比较。

GCC标准库实现在内部使用T**来表示地图。每个数据块都是T*,分配了一些固定大小__deque_buf_size(取决于sizeof(T))。

答案 1 :(得分:18)

想象它是矢量的矢量。只有它们不是标准std::vector s。

外部向量包含指向内部向量的指针。当通过重新分配更改其容量时,而不是像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的开头或结尾处的那些。

答案 2 :(得分:15)

  

deque =双端队列

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

Deque 通常实现为vector vectors(向量列表无法提供恒定时间随机访问)。虽然辅助向量的大小取决于实现,但常见的算法是使用以字节为单位的常量大小。

答案 3 :(得分:10)

(这是我在another thread中给出的答案。本质上我认为即使是相当天真的实现,使用单个vector,也符合“常量非摊销推送_的要求” {front,back}“。你可能会感到惊讶,并认为这是不可能的,但我在标准中找到了其他相关的引用,以令人惊讶的方式定义了上下文。请耐心等待;如果我在这方面犯了错误回答,找出我说的正确的东西以及逻辑分解的地方会非常有帮助。)

在这个答案中,我并不是要确定一个好的实现,我只是想帮助我们解释C ++标准中的复杂性要求。我引用N3242,根据Wikipedia,这是最新的免费C ++ 11标准化文档。 (它似乎与最终标准的组织方式不同,因此我不会引用确切的页码。当然,这些规则可能在最终标准中有所改变,但我认为这种情况并未发生。)

使用deque<T>可以正确实现vector<T*>。所有元素都复制到堆上,指针存储在向量中。 (稍后有关矢量的更多信息)。

为什么T*代替T?因为标准要求

  

“deque两端的插入使所有迭代器无效   到deque,但对引用的有效性没有影响   deque的元素。

(我的重点)。 T*有助于满足这一要求。它也有助于我们满足这一要求:

  

“在双端队列的开头或末尾插入单个元素总是.....导致单次调用T 的构造函数。”

现在为(有争议的)位。为什么要使用vector来存储T*?它为我们提供随机访问,这是一个良好的开端。让我们暂时忘记矢量的复杂性,并仔细考虑:

标准谈到“包含对象的操作次数”。对于deque::push_front,这显然是1,因为只构造了一个T对象,并且以任何方式读取或扫描现有T个对象中的零个。这个数字1显然是一个常数,与目前在双端队列中的对象数量无关。这让我们可以这样说:

'对于我们的deque::push_front,包含对象上的操作数(Ts)是固定的,并且与双端队列中已有的对象数无关。'

当然,T*上的操作次数不会那么好。当vector<T*>变得太大时,它将被重新分配,并且将复制许多T*。是的,T*上的操作数量会有很大差异,但T上的操作数量不会受到影响。

为什么我们关心T上的计数操作与T*上的操作计数之间的区别?这是因为标准说:

  

本节中的所有复杂性要求仅根据所包含对象的操作次数来说明。

对于deque,包含的对象是T,而不是T*,这意味着我们可以忽略复制(或重新分配)T*的任何操作。 / p>

我没有多说过一个矢量在双端队列中的表现。也许我们会把它解释为一个循环缓冲区(向量总是占用它的最大值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*上的操作,因此无关紧要。如果标准指的是复杂性的“传统”理论概念,那么它们就不会明确地将自己局限于“所包含对象的操作次数”。我是否过度解释这句话?

答案 4 :(得分:6)

虽然标准没有强制要求任何特定的实现(只有恒定时间随机访问),但是deque通常被实现为连续内存“页面”的集合。根据需要分配新页面,但您仍然可以随机访问。与std::vector不同,您不会承诺数据是连续存储的,但是像矢量一样,中间插入需要大量重新定位。

答案 5 :(得分:6)

从总体上看,您可以将deque视为double-ended queue

deque overview

deque中的数据通过固定大小的矢量块存储,即

map指针(这也是向量的一部分,但其大小可能会改变)

deque internal structure

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

enter image description here

下面,我将提供__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;
}
迭代器跳过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的常用功能

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,现在将3个元素push_back推到i_deque

i_deque.push_back(1);
i_deque.push_back(2);
i_deque.push_back(3);

它的内部结构如下:

enter image description here

然后再次push_back,它将调用分配新块:

push_back(3)

enter image description here

如果我们push_front,它将在上一个start之前分配新的块

enter image description here

请注意,当push_back元素放入双端队列时,如果所有图和块均已填充,将导致分配新图并调整块。但是上面的代码可能足以让您理解deque

答案 6 :(得分:3)

我正在阅读Adam Drozdek撰写的“C ++中的数据结构和算法”,并发现这很有用。 HTH。

  

STL deque的一个非常有趣的方面是它的实现。 STL deque不是作为链接列表实现的,而是作为指向块或数据数组的指针数组实现的。块的数量根据存储需求而动态变化,并且指针数组的大小也相应地改变。

你可以注意到中间是指向数据的指针数组(右边的数据块),你也可以注意到中间的数组是动态变化的。

一张图片胜过千言万语。

enter image description here

答案 7 :(得分:0)

deque 可以实现为固定大小数组的循环缓冲区:

  • 使用循环缓冲区,这样我们就可以通过添加/删除复杂度为 O(1) 的固定大小的数组在两端增长/缩小
  • 使用固定大小的数组,因此很容易计算索引,因此可以通过带有两个指针取消引用的索引进行访问 - 也是 O(1)