是否有一个特定的数据结构,C ++ STL中的deque应该实现,或者是一个deque只是这个模糊的概念,一个数组可以从前面和后面增长,然而实现选择?< / p>
我曾经常常认为deque是circular buffer,但我最近读的是C ++引用here,听起来像deque是某种数组的数组。它似乎不是一个普通的旧循环缓冲区。它是gap buffer,还是some other variant of growable array,还是只是依赖于实现?
答案的更新和摘要:
似乎普遍的共识是,双端队列是一种数据结构,以便:
如果我们将第一个条件设为“非摊销的恒定时间”,似乎没有人知道如何得到第一和第四条件的组合。链表实现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)访问。
答案 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_front
和push_back
比vector
效率更高(除非你使用MSVC实现,因为阵列的容量非常小,因此速度非常慢......)
老实说,我知道没有数据结构或数据结构组合可以同时满足:
和
我确实知道一些“近距离”比赛:
deque
答案 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}} - 复杂性,这是常量(非摊销)。
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 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
的核心代码,主要包括两部分:
迭代器
关于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();
}
请注意,用于计算块大小的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;
}
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