什么是STL的双端队列?

Zon*_*nko 173 c++ stl deque

我正在查看STL容器并试图弄清楚它们到底是什么(即使用的数据结构),并且deque阻止了我:我起初认为它是一个双链表,它允许从两端插入和删除恒定时间,但我对运营商[]在恒定时间内做出的承诺感到不安.在链表中,任意访问应该是O(n),对吗?

如果它是一个动态数组,它如何在恒定时间内添加元素?应该提到的是,可能会发生重新分配,并且O(1)是一个摊销成本,就像向量一样.

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

Kon*_*lph 165

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

deque的内存布局示意图

还有的性能有很大的分析和如何比较的vector超过CodeProject上.

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

  • 这就是我学习deque的定义,但是这样,它无法保证持续的时间访问,因此必然会缺少某些东西. (23认同)
  • @stefaanv,@ Konrad:我见过的C++实现使用了一个指向固定大小数组的指针数组.这实际上意味着push_front和push_back实际上不是常数,但是随着智能增长因素的推移,你仍然可以分摊不变的时间,所以O(1)不是那么错误,并且在实践中它比vector更快,因为你正在交换单指针而不是整个对象(指针比指针少). (12认同)
  • @JeremyWest为什么不呢?索引访问转到第i/B块中的第i%B元素(B =块大小),显然是O(1).您可以在分摊的O(1)中添加新块,因此添加元素在结尾处分摊O(1).除非需要添加新块,否则在开头添加新元素是O(1).在开头添加一个新块不是O(1),是的,它是O(N),但实际上它有一个非常小的常数因子,因为你只需要移动N/B指针而不是N个元素. (12认同)
  • 仍然可以进行恒定时间访问.只是,如果您需要在前面分配一个新块,请在主向量上推回一个新指针并移动所有指针. (5认同)
  • 如果地图(队列本身)是双端列表,我看不出它如何允许O(1)随机访问.它可以实现为循环缓冲区,这将允许循环缓冲区调整大小更高效:只复制指针而不是队列中的所有元素.看来这仍然是一个小小的好处. (4认同)
  • @Wernight是的,我的部分内容完全是胡说八道,评论中的很多讨论都是错误的.deque是一个向量的向量,纯粹而简单(实际上,stdlibc ++实现只是将它作为`T**`).我已经修好了那个部分. (3认同)
  • 如果保留指向第一个有效块的指针,则在开头添加新块可以分摊O(1),并且外部级别向量的每次重新分配在分配的块的开头和结尾都留下空白空间.标准的`vector`不会这样做,但这不是一个很难修改. (3认同)
  • @HCSF 分配一个块*是* O(1),因为它的大小是恒定的。但更重要的是,即使分配可变大小的块(例如,大小为 *m*)也可以摊销 O(1),如果它仅每 1 / *m* 操作发生一次。 (2认同)

Mar*_*som 20

想象它是矢量的矢量.只有他们不是标准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.

  • 您可以将内部向量描述为具有固定的*容量* (2认同)

Alo*_*ave 16

deque =双端队列

一个可以向任一方向生长的容器.

双端队列通常实现为vectorvectors(向量的列表不能给恒定时的随机接入).虽然辅助向量的大小取决于实现,但常见的算法是使用以字节为单位的常量大小.

  • @MooingDuck如果第一个块从上到下而不是自下而上增长,那么很容易满足要求.显然,标准的`vector`不会这样做,但它是一个足够简单的修改来实现它. (4认同)
  • 它内部不是_quite_向量.内部结构可以在_beginning_以及结束时分配但未使用的容量 (3认同)
  • @Als:我不认为任何东西的“数组”或任何东西的“向量”可以保证摊销的“O(1)”push_front。这两个结构的内部至少必须能够有一个“O(1)”的push_front,这是“数组”和“向量”都无法保证的。 (3认同)
  • @ Mooing Duck,push_front和push_back都可以使用单个矢量结构在分摊的O(1)中轻松完成.它只是对循环缓冲区的一点点记录,仅此而已.假设你有一个容量为1000的常规向量,其中有100个元素位于0到99位置.现在当push_Front发生时,你只需按下结束位置,即位置999,然后是998等,直到两端相遇.然后你重新分配(指数增长以保证amortizet常数时间)就像你使用普通向量一样.所以有效地你只需要一个额外的指针指向第一个el. (3认同)

Aar*_*aid 11

(这是我在另一个帖子中给出的答案.基本上我在争论即使是相当天真的实现,使用单一vector,符合"常量非摊销push_ {front,back}"的要求.你可能会感到惊讶,并认为这是不可能的,但我在标准中找到了其他相关的引用,以令人惊讶的方式定义了上下文.请耐心等待;如果我在这个答案中犯了错误,那么识别哪些东西会非常有帮助我已经说得对了,我的逻辑已经崩溃了.)

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

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

为什么T*而不是T?因为标准要求

"在双端队列的任一端插入都会使deque的所有迭代器无效,但对deque元素的引用的有效性没有影响. "

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

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

现在为(有争议的)位.为什么用a 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*,意味着我们可以忽略任何复制(或重新分配)a的操作T*.

我没有多说过一个向量在双端队列中的表现.也许我们会将它解释为循环缓冲区(向量总是占用它的最大值capacity(),然后在向量满时将所有内容重新分配到更大的缓冲区.细节无关紧要.

在最后几段中,我们分析deque::push_front了deque中对象数量与push_front对包含T对象执行的操作数量之间的关系.我们发现它们彼此独立.由于标准要求复杂性就操作而言T,我们可以说这具有不变的复杂性.

是的,Operations-On-T*-Complexity是摊销的(由于vector),但我们只对T-Complexity操作感兴趣,这是常数(非摊销).

vector :: push_back或vector :: push_front的复杂性在此实现中无关紧要; 这些考虑涉及操作T*,因此无关紧要.如果标准指的是复杂性的"传统"理论概念,那么它们就不会明确地将自己局限于"所包含对象的操作次数".我是否过度解释这句话?

  • 这似乎很像欺骗我!当您指定操作的复杂性时,不会仅对数据的某些部分执行此操作:您希望了解正在调用的操作的预期运行时,而不管其操作是什么.如果我按照你关于T操作的逻辑,这意味着你可以检查每次执行操作时每个T*的值是否为素数,并且因为你没有触摸Ts,所以仍然尊重标准.你能指出你的报价来自哪里吗? (8认同)
  • 这是一个非常有趣的解释,但是通过这个逻辑,`list`也可以实现为指针的`vector`(插入到中间将导致*single*copy构造函数调用,无论列表大小如何,并且可以忽略指针的"O(N)"混洗,因为它们不是T上的操作. (7认同)
  • 我认为标准编写者知道他们不能使用传统的复杂性理论,因为我们没有一个完全指定的系统,我们知道,例如,内存分配的复杂性.无论列表的当前大小如何,假装可以为"list"的新成员分配内存是不现实的.如果列表太大,分配将很慢或将失败.因此,据我所知,委员会决定只指定可以客观计算和衡量的操作.(PS:关于另一个答案,我有*另一个*理论.) (2认同)
  • 这是一个很好的语言律师(尽管我不会试图确定它是否真的正确,或者标准中是否有一些微妙的地方禁止这种实现)。但这在实践中并不是有用的信息,因为 (1) 常见的实现不会以这种方式实现 `deque` 并且 (2) 当计算算法复杂性对编写高效的程序。 (2认同)
  • @Kyle Strand,常见的实现将指向单个 T 的指针替换为指向本质上固定的 T 数组的指针(加上与指针或数组一起保存的少量簿记数据)。它们仍然具有相同的渐近特征,只是常数通常会更有利。 (2认同)

Jay*_*llo 11

从概述,你能想到dequedouble-ended queue

deque概述

在DATAS deque由固定大小的矢量的chuncks,这是存储

由a指针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
}
Run Code Online (Sandbox Code Playgroud)

主要部分代码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;
}
Run Code Online (Sandbox Code Playgroud)

下面我将给你核心代码deque,主要是关于三个部分:

  1. 迭代器

  2. 如何构建一个 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();
}
Run Code Online (Sandbox Code Playgroud)

注意,chunk_size()计算块大小的函数,你可以想到它返回8以简化这里.

operator* 获取块中的数据

reference operator*()const{
    return *cur;
}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud) 迭代器跳过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);
}
Run Code Online (Sandbox Code Playgroud)

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();
}
Run Code Online (Sandbox Code Playgroud)

假设i_deque有20个int元素,0~19其块大小为8,现在push_back 3个元素为i_deque:

i_deque.push_back(0);
i_deque.push_back(1);
i_deque.push_back(2);
Run Code Online (Sandbox Code Playgroud)

它的内部结构如下:

在此输入图像描述

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

push_back(3)
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

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

在此输入图像描述

注意当push_back元素进入双端队列时,如果所有的地图和块都被填充,它将导致分配新的地图,并调整块.但是上面的代码可能足以让你理解deque.


Kel*_*loo 9

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

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

您可以注意到中间是指向数据的指针数组(右侧的块),并且您还可以注意到中间的数组是动态变化的。

一张图片胜过千言万语。

在此处输入图片说明


Ker*_* SB 6

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

  • 或中间的删除需要大量的重新安置 (4认同)