具有O(1)出队和O(任意)入队的优先级队列

5

我正在用C++编写一个应用程序,对于优先队列来说,具有O(1)的Dequeue操作至关重要,而Enqueue的复杂度则不那么重要(当然,除非它变成了n^2或2^n)。

最初我使用了链表。它非常适合Dequeue(O(1)),而且具有很好的Enqueue复杂度。唯一的问题是排序它。并不是使用O(n)复杂度的插入排序适合我的需求,而是排列链表本身非常麻烦。这太慢了。

向量根本不好。Dequeue将是O(n),需要把所有元素向后移动一位。Enqueue仍然是O(n),但速度要快得多。

你能提供更高效的方法吗?谢谢。


在你的问题中,你说,“Enqueue 的复杂度并不是很重要(当然,除非它变成了 n^2 或 2^n)”。然后在注释中,你又说重新分配向量(即 O(n))太慢了。下定决心吧 :-) - Steve Jessop
我不明白两件事情:(1)使用归并排序对链表进行排序非常容易。(2)如果将所有对象都加入到链表中,就无需对其进行排序,因为它已经是有序的。使用简单插入法将元素插入到正确位置的链表已经具有O(1)出队和O(n)入队的时间复杂度,无需进行排序。这很奇怪。 - Antti Huima
对我来说,出队操作更重要。但是我不希望入队成为瓶颈。这将是一个非常大的软件中的一部分。而且列表可能包含数百个元素... - Francesco Boffa
@antti.huima 我正在使用插入排序,因为它对于已经排序的列表比其他排序更有效率。当然,把元素放在正确的位置更有效率。我只是没有想到 :) - Francesco Boffa
1
@Alfa:请注意,长期来看,每个元素只会被添加一次并且最多被移除一次。因此,堆可以提供最佳的整体性能,因为这两个操作都可以达到 O(log n)。除非您详细说明了使删除操作比这更快速度如此重要的具体要求,否则您将无法获得最佳建议。可能是为了使删除器更加响应 I/O 或 GUI 等方面。与堆相比,您希望放弃整体性能以加快删除速度。但是您没有说整体上应该慢多少。 - Steve Jessop
5个回答

14

反向排序的 vector 具有O(1)的pop_back和O(n)的插入。


好的,这很好,我简直不敢相信以前我没有想到过这个。然而,当向量已满时,这将需要重新分配内存。仍然太慢了,抱歉。 - Francesco Boffa
2
@AlfaOmega08:我没有时间来证明这一点,但我怀疑定期重新分配向量可能比列表执行的所有小型分配更快。 - Fred Foo
1
@larsmans:无论如何,如果重新分配是vector唯一的问题,那么你可以尝试使用deque - Steve Jessop

10

您可以将队列存储为排序的链接列表。删除队列前端元素的时间复杂度为O(1),在正确位置插入元素的时间复杂度为O(n)

但是对链接列表进行排序很麻烦。速度很慢。

每次插入后不必执行完整的排序。您只需遍历(已排序)列表以查找新元素的正确位置,并在那里插入它。遍历的时间复杂度为O(n),插入的时间复杂度为O(1)


是的,我猜我的排序方法不太聪明...知道它已经排序过会有所不同。这是目前的赢家 :) - Francesco Boffa
这是正确的答案,但我无法给它点赞,因为它完全是平凡的。 - Antti Huima
我提供了一个具有O(1)删除最小值[弹出]和O(log n)插入[推入]的解决方案。请考虑接受我的答案。 - A T

1

如果您愿意从文献中实现,那么我有一个更好的解决方案。

最坏情况复杂度

删除: O(1)

删除最小值: O(1)

查找最小值: O(1)

插入: O(log n)

参考资料

如果允许MELD花费线性时间,则可以使用Dietz和Raman的指针搜索树[3]来支持最坏情况下常数时间的DELETE-MIN。 通过使用他们的数据结构MAKEQUEUEFINDMINDELETEMINDELETE可以在最坏情况下支持时间O(1),INSERT在最坏情况下支持时间O(log n),MELD在最坏情况下支持时间O(n)。

Brodal, Gerth Stølting. ‘Fast Meldable Priority Queues’. 在第四届算法和数据结构国际研讨会论文集中,282-290页。WADS '95。伦敦,英国:Springer-Verlag,1995年。

[3]: Dietz, Paul F, and Rajeev Raman. ‘A Constant Update Time Finger Search Tree’. 信息处理信件52卷,第3期(1994年):147-154。

虽然这采用了RAM 计算模型

我们的数据结构使用随机访问机器(RAM)模型,具有单位成本度量和对数字大小;

最近,Pointer-Machine 计算模型中提出了一个解决方案[1]。它具有 O(1) 的 get-min、extract-min、get-max、extract-max 和 O(log n) 的 insert。

[1]: Brodal, Gerth Stølting, George Lagogiannis, Christos Makris, Athanasios Tsakalidis, and Kostas Tsichlas. ‘Optimal Finger Search Trees in the Pointer Machine’. J. Comput. Syst. Sci. 67, no. 2 (September 2003): 381–418.


0
Boost现在包括Boost.Heap,一个支持优先队列操作的堆数据结构库。此页面提供了每个提供的数据结构的核心操作的摊销复杂度表格。斐波那契堆具有以下特点:O(1) push,O(log(N)) pop,O(1) increase,以及(如果需要)O(log(N)) decrease。

它们中没有一个具有 O(1) 弹出,而问题声称这是“关键”的。当然,有时人们会犯错误,并认为他们需要 O(1),而实际上 O(log n) 是可以的。在实践中,log n < 64,因此 O(log n) 可以看作是较慢的 O(1) - Steve Jessop
@SteveJessop:是的,没错。我知道OP说过O(1) pop,但也许OP之所以要求优先队列,可能有其原因。 - Daniel Trebbien
2
“优先队列”描述了提问者想要的功能,而不是特定类型的数据结构。底层数据结构(堆或其他)以及由此产生的复杂性是重要的实现细节,但它们并不影响结果是否为优先队列。如果提问者确实需要具有O(1)弹出的优先队列,则通常的堆将被排除在外,当然,推送的性能也会相应受到影响。 - Steve Jessop
优先队列 != 堆。前者是一个提供某些操作的 ADT,但没有指定它们应该如何实现。 - Fred Foo

0

可以将平衡二叉搜索树与链表相结合。树的每个元素都有指向其子节点的链接,还有指向前一个和后一个元素的链接。这样你就可以拥有:

O(lg n) 插入、删除、搜索;O(1) - 提取最小值和最大值

另一种可能性是使用跳表,如果您不介意使用随机结构,那么您也将拥有:

O(lg n) 插入、删除、搜索;O(1) - 提取最小值和最大值


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接