我实现了一种算法,我使用优先级队列。 我被这个问题所激励: 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
。
也许问题是多图的重组太多了?
答案 0 :(得分:4)
总结一下:您的运行时配置文件涉及从抽象优先级队列中删除和插入元素,同时尝试使用std::priority_queue
和std::multimap
作为实际实现。
插入优先级队列和多重映射都具有大致相同的复杂性:logarithmic。
但是,从多图和优先级队列中删除下一个元素会有很大的不同。使用优先级队列,这将是一个持续复杂的操作。底层容器是一个向量,你要从向量中移除最后一个元素,这将主要是一个没什么汉堡。
但是使用multimap,您可以从多图的一个极端端移除元素。
多图的典型底层实现是平衡的红/黑树。从多图的一个极端重复元素移除很有可能使树偏斜,需要频繁地重新平衡整个树。这将是一项昂贵的操作。
这可能是您看到显着性能差异的原因。
答案 1 :(得分:2)
我认为主要区别在于两个事实:
因此,虽然两者上的操作的理论时间复杂度是相同的O(log(size))
,但我认为来自erase
的{{1}},并且重新平衡RB树执行更多操作,它只需要移动更多的元素。 (注意:RB-tree不是必需的,但通常被选为multimap
的基础容器)
multimap
)。我怀疑重新平衡也较慢,因为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
继续为每个 insert
和 erase
支付内存管理成本。
可以通过使用基于内存池的自定义分配器来减轻 map 内存管理的缺点。这也为您提供了与优先级队列相当的缓存命中率。当您的对象移动或复制时,它的性能甚至可能优于 priority-queue
。