究竟什么数据结构是C ++中的deques?

时间:2011-12-24 23:13:54

标签: c++ data-structures

是否有一个特定的数据结构,C ++ STL中的deque应该实现,或者是一个deque只是这个模糊的概念,一个数组可以从前面和后面增长,然而实现选择?< / p>

我曾经常常认为deque是circular buffer,但我最近读的是C ++引用here,听起来像deque是某种数组的数组。它似乎不是一个普通的旧循环缓冲区。它是gap buffer,还是some other variant of growable array,还是只是依赖于实现?

答案的更新和摘要

似乎普遍的共识是,双端队列是一种数据结构,以便:

  • 插入或删除元素的时间应该在列表的开头或结尾处是常量,并且在其他地方最多是线性的。如果我们将此解释为真正的恒定时间而非摊销的恒定时间,正如有人评论,这似乎具有挑战性。有些人认为我们不应将此解释为非摊销的常数时间。
  • “deque要求任何插入都应保持对成员元素的任何引用有效。迭代器可以无效,但成员本身必须保留在内存中的相同位置。”正如有人评论的那样:只需将成员复制到堆上的某个位置并将T *存储在引擎盖下的数据结构中就足够了。
  • “在双端队列的开头或末尾插入单个元素总是需要一个恒定的时间并导致对T的构造函数的单个调用。”如果数据结构在引擎盖下存储T *,也将实现T的单个构造函数。
  • 数据结构必须具有随机访问权限。

如果我们将第一个条件设为“非摊销的恒定时间”,似乎没有人知道如何得到第一和第四条件的组合。链表实现1)但不是4),而典型的循环缓冲实现4)但不实现1)。我想我的实现可以满足以下两个要求。评论

我们从其他人建议的实现开始:我们分配一个数组并开始从中间放置元素,在前面和后面留下空间。在这个实现中,我们跟踪中心在前后方向上有多少元素,调用那些值F和B.然后,让我们用一个两倍于原始大小的辅助数组来扩充这个数据结构。数组(所以现在我们浪费了大量的空间,但渐近的复杂性没有变化)。我们还将从中间填充这个辅助数组,并给它类似的值F'和B'。策略是这样的:每当我们在给定方向上向主阵列添加一个元素时,如果F> 1。 F'或B&gt; 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)访问。

7 个答案:

答案 0 :(得分:13)

具体实施。所有deque要求是在开始/结束时的恒定时间插入/删除,并且在其他地方最多是线性的。元素不需要是连续的。

大多数实现都使用可以描述为展开列表的内容。固定大小的数组在堆上分配,指向这些数组的指针存储在属于deque的动态大小的数组中。

答案 1 :(得分:10)

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)两端插入

我确实知道一些“近距离”比赛:

  • Amortized O(1)插入可以使用动态数组完成,您可以在其中写入,这与deque
  • 的“参考保留”语义不兼容
  • B + Tree可以通过索引而不是按键提供访问,时间接近常量,但复杂度为O(log N),用于访问和插入(使用小常量),它需要使用中间级节点中的Fenwick树。
  • 手指树可以进行类似的调整,但它再次真的是O(log N)。

答案 2 :(得分:5)

使用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- {感兴趣{1}} - 复杂性,这是常量(非摊销)。

结语:vector :: push_back或vector :: push_front的复杂性与此实现无关;这些考虑涉及T上的操作,因此无关紧要。

答案 3 :(得分:4)

(让这个答案成为社区维基。请陷入困境。)

首先要做的事情是:deque要求对前面或后面的任何插入都应保持对成员元素的任何引用有效。迭代器无效是可以的,但成员本身必须保持在内存中的相同位置。只需将成员复制到堆上的某个位置并将T*存储在引擎盖下的数据结构中即可。请参阅此其他StackOverflow问题“About deque<T>'s extra indirection

vector不保证保留迭代器或引用,而list保留两者。)

所以,让我们把这个'间接'视为理所当然,看看问题的其余部分。有趣的是从列表的开头或结尾插入或删除的时间。首先,看起来deque可以通过vector轻松实现,也许可以将其解释为circular buffer

但是 deque必须满足“在一个元素的开头或结尾插入一个元素 deque总是占用一个恒定的时间并导致对T的构造函数的单个调用。“

由于我们已经提到的间接性,很容易确保只有一个构造函数调用,但挑战是保证恒定的时间。如果我们可以使用常量摊销的时间,这将很容易,这将允许简单的vector实现,但它必须是恒定的(非摊销的)时间。

答案 4 :(得分:0)

我对deque的理解

它分配&#39; n&#39;来自堆的空连续对象作为第一个子数组。 其中的对象在插入时由头指针添加一次。

当头指针到达数组的末尾时,它 分配/链接一个新的非连续子数组并在那里添加对象。

它们在提取时被尾指针删除一次。 当尾指针完成对象的子数组时,它会移动 转到下一个链接的子数组,并释放旧的。

头部和尾部之间的中间对象永远不会被deque在内存中移动。

随机访问首先确定哪个子阵列具有 对象,然后从它在子阵列中的相对偏移量中访问它。

答案 5 :(得分:0)

这是对用户重力评论2阵列解决方案的挑战的答案。

  • 此处讨论了一些细节
  • 提出改进建议

详情讨论: 用户&#34;重力&#34;已经给出了一个非常简洁的总结。 &#34;重力&#34;还要求我们评论平衡两个数组之间的元素数量的建议,以实现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将完全变空。

答案 6 :(得分:0)

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

  3. 的简单函数

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;
}

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