c ++为什么std :: multimap比std :: priority_queue

时间:2017-01-23 13:38:44

标签: c++ std priority-queue multimap

我实现了一种算法,我使用优先级队列。 我被这个问题所激励: Transform a std::multimap into std::priority_queue

我将存储多达1000万个具有特定优先级值的元素。

然后我想迭代直到队列为空。 每次检索到一个元素时,它也会从队列中删除。

在此之后我重新计算元素pririty值,因为之前的迭代它可以改变。

如果值确实增加,我将元素againg插入队列中。 这通常取决于进展。 (在前25%它没有发生,在接下来的50%中确实会发生,在过去的25%中会发生多次)。

收到下一个元素而不重新插入后,我将处理它。这对于我不需要此元素的优先级值,而是此元素的技术ID。

这就是我直觉选择std::multimap来实现此目的的原因,使用.begin()获取第一个元素,.insert()插入它,.erase()删除它。 此外,我没有直观地选择std::priority_queue,因为此主题的其他问题回答std::priority_queue最有可能仅用于单个值,而不用于映射值。

在阅读上面的链接后,我使用优先级队列类比重新实现了链接中的另一个问题。 我的运行时间似乎不是那么不平等(大约一个小时就有10个mio元素)。 现在我想知道为什么std::priority_queue更快。

由于许多重新插入,我实际上希望更快std::multimap。 也许问题是多图的重组太多了?

4 个答案:

答案 0 :(得分:4)

总结一下:您的运行时配置文件涉及从抽象优先级队列中删除和插入元素,同时尝试使用std::priority_queuestd::multimap作为实际实现。

插入优先级队列和多重映射都具有大致相同的复杂性:logarithmic。

但是,从多图和优先级队列中删除下一个元素会有很大的不同。使用优先级队列,这将是一个持续复杂的操作。底层容器是一个向量,你要从向量中移除最后一个元素,这将主要是一个没什么汉堡。

但是使用multimap,您可以从多图的一个极端端移除元素。

多图的典型底层实现是平衡的红/黑树。从多图的一个极端重复元素移除很有可能使树偏斜,需要频繁地重新平衡整个树。这将是一项昂贵的操作。

这可能是您看到显着性能差异的原因。

答案 1 :(得分:2)

我认为主要区别在于两个事实:

  1. 优先级队列对元素顺序的约束较弱。它不必排序整个范围的键/优先级。 Multimap,必须提供。优先级队列只需要保证1st / top元素最大。
  2. 因此,虽然两者上的操作的理论时间复杂度是相同的O(log(size)),但我认为来自erase的{​​{1}},并且重新平衡RB树执行更多操作,它只需要移动更多的元素。 (注意:RB-tree不是必需的,但通常被选为multimap 的基础容器)

    1. 优先级队列的基础容器在内存中是连续的(默认情况下它是multimap)。
    2. 我怀疑重新平衡也较慢,因为RB-tree依赖于节点(相对于向量的连续内存),这使得它容易出现缓存未命中,尽管必须记住堆上的操作不是以迭代方式完成的,它正在跳过矢量。我想要确定一个人必须对其进行分析。

      以上几点适用于插入和删除。我会说区别在于vector符号中丢失的常数因素。这是直观的思考。

答案 2 :(得分:1)

地图变慢的抽象,高级解释是它做得更多。它始终保持整个结构的排序。此功能需要付费。如果您使用的数据结构不能对所有元素进行排序,则不会支付该费用。

算法解释:

为了满足复杂性要求,必须将映射实现为基于节点的结构,而优先级队列可以实现为动态数组。 std::map的实现是一个平衡的(通常是红黑)树,而std::priority_queue是一个以std::vector作为默认底层容器的堆。

堆插入通常非常快。与平衡树的O(log n)相比,插入堆中的平均复杂度为O(1)(尽管最差情况相同)。创建n个元素的优先级队列具有O(n)的最坏情况复杂度,而创建平衡树是O(n log n)。请参阅更多深度比较:Heap vs Binary Search Tree (BST)

其他,实施细节:

与基于节点的结构(如树或列表)相比,数组通常更有效地使用CPU缓存。这是因为阵列的相邻元素在存储器中相邻(高存储器局部性),因此可以适合单个高速缓存行。然而,链接结构的节点存在于存储器中的任意位置(低存储器局部性)中,并且通常仅一个或极少数位于单个高速缓存行内。现代CPU的计算速度非常快,但内存速度却是瓶颈。这就是基于数组的算法和数据结构往往明显快于基于节点的原因。

答案 3 :(得分:1)

虽然我同意@eerorika 和@luk32,但值得一提的是,在现实世界中,当使用默认的 STL 分配器时,内存管理成本很容易超过一些数据结构维护操作,例如更新指针以执行树回转。根据实现,内存分配本身可能涉及树维护操作,并可能触发系统调用,而系统调用会变得更加昂贵。

multi-map 中,内存分配和释放分别与每个 insert()erase() 相关联,这通常会导致比算法中的额外步骤更高数量级的缓慢。

priority-queue 然而,默认情况下使用 vector ,它只在容量耗尽时触发内存分配(尽管这是一个更广泛的分配,它涉及将所有存储的对象移动到新的内存位置)。在您的情况下,几乎所有分配都只发生在 priority-queue 的第一次迭代中,而 multi-map 继续为每个 inserterase 支付内存管理成本。

可以通过使用基于内存池的自定义分配器来减轻 map 内存管理的缺点。这也为您提供了与优先级队列相当的缓存命中率。当您的对象移动或复制时,它的性能甚至可能优于 priority-queue