【发布时间】:2012-01-27 11:33:30
【问题描述】:
是否存在 C++ STL 中的双端队列应该实现的特定数据结构,或者双端队列只是一个可从前后增长的数组的模糊概念,无论实现选择如何都可以实现?
我过去一直认为双端队列是 circular buffer,但我最近阅读了 C++ 参考资料 here,听起来双端队列是某种数组数组。它似乎不是一个普通的旧循环缓冲区。那么是gap buffer,还是some other variant of growable array,还是只是依赖于实现?
答案更新和总结:
似乎普遍的共识是双端队列是这样一种数据结构:
- 插入或删除元素的时间在列表的开头或结尾应该是恒定的,并且在其他地方最多是线性的。如果我们将其解释为真正的常数时间而不是摊销的常数时间,就像有人 cmet 那样,这似乎具有挑战性。有些人认为我们不应该将其解释为非摊销常数时间。
- “双端队列要求任何插入都应保持对成员元素的任何引用有效。迭代器无效是可以的,但成员本身必须保留在内存中的同一位置。”就像某人一样:这很容易,只需将成员复制到堆上的某个位置并将 T* 存储在引擎盖下的数据结构中。
- “在双端队列的开头或结尾插入单个元素总是需要恒定的时间,并且会导致对 T 的构造函数的单个调用。”如果数据结构在后台存储 T*,也将实现 T 的单个构造函数。
- 数据结构必须具有随机访问权限。
如果我们将第一个条件设为“非摊销常数时间”,似乎没有人知道如何获得第一个和第四个条件的组合。链表实现 1) 但不是 4),而典型的循环缓冲区实现 4) 但不是 1)。我想我有一个可以满足以下两个方面的实现。评论?
我们从其他人建议的实现开始:我们分配一个数组并从中间开始放置元素,前后都留有空间。在这个实现中,我们跟踪前后方向的中心有多少元素,将这些值称为 F 和 B。然后,让我们用一个两倍于原始大小的辅助数组来扩充这个数据结构数组(所以现在我们浪费了大量空间,但渐近复杂度没有变化)。我们还将从中间填充这个辅助数组,并给它类似的值 F' 和 B'。策略是这样的:每次我们在给定方向上向主数组添加一个元素时,如果 F > F' 或 B > B'(取决于方向),最多复制 两个 值从主阵列到辅助阵列,直到 F' 赶上 F(或 B' 赶上 B)。因此,插入操作涉及将 1 个元素放入主数组并将最多 2 个从主数组复制到辅助数组,但它仍然是 O(1)。当主阵列变满时,我们释放主阵列,使辅助阵列成为主阵列,并创建另一个 2 倍大的辅助阵列。这个新的辅助数组以 F' = B' = 0 开始,并且没有复制任何内容(因此,如果堆分配的复杂度为 O(1),则调整大小操作为 O(1))。由于辅助节点为添加到主节点的每个元素复制 2 个元素,并且主节点开始时最多半满,因此当主节点再次耗尽空间时,辅助节点不可能没有赶上主节点。删除同样只需要从主要元素中删除 1 个元素,从辅助元素中删除 0 或 1。因此,假设堆分配为 O(1),则此实现满足条件 1)。我们将数组设为 T* 并在插入时使用 new 以满足条件 2) 和 3)。最后,4) 实现了,因为我们使用的是数组结构,可以轻松实现 O(1) 访问。
【问题讨论】:
-
你可能有兴趣阅读this question我刚才问过。
-
它在任何意义上都不是一个循环缓冲区
-
间隙缓冲区对于双端队列来说效率很低。
-
@TomalakGeret'kal,使用循环缓冲区没有任何问题。事实上,它们是一个相当合理的实现。在您查看 Pubby 答案的评论线程时,您会看到很多争论。似乎有些人确信必须是一个简单的链表,具有非随机访问。让更多的人看到这一点会很好。
-
噢! N3242 中的以下句子是相关的,让我停下来思考(即我可能错了)。 “在双端队列的任何一端插入都会使双端队列的所有迭代器无效,但对双端队列元素引用的有效性没有影响。”和“在双端队列的开头或结尾插入单个元素总是需要恒定的时间并导致对 T 的构造函数的一次调用。”
标签: c++ data-structures