二元堆通常用于例如二元堆中。优先队列。基本思想是不完整的堆排序:你将数据排序“足够”以快速排出顶部元素。
虽然理论上4-ary堆比二进制堆差,但它们也有一些好处。例如,它们将需要较少的堆重组操作(因为堆较浅),而在每个级别上显然需要更多的比较。但是(这可能是他们的主要好处?)他们可能有更好的CPU缓存局部性。所以一些消息来源说3-ary和4-ary堆在实践中胜过斐波那契和二进制堆。 它们不应该更难实现,其他情况只是一些额外的if
个案例。
是否有人尝试使用4-ary堆(和3-ary)作为优先级队列并进行了一些基准测试? 在Java中,在广泛地对它们进行基准测试之前,你永远不知道它们是更快还是更慢。 从我通过谷歌找到的所有内容来看,它可能是语言和用例依赖。一些消息来源说他们发现3-ary对他们来说效果最佳。
更多要点:
PriorityQueue
显然是二进制堆。但是,例如,该类也缺乏批量加载和批量修复支持,或者replaceTopElement
可以产生巨大的差异。例如,批量加载是O(n)
而不是O(n log n)
;在添加更多候选人之后,批量修复基本相同。跟踪堆的哪些部分无效可以使用单个整数来完成。 replaceTopElement
比poll
+ add
便宜得多(只考虑如何实施民意调查:将最后一个元素替换为最后一个元素)答案 0 :(得分:2)
根据@ ErichSchubert的建议,我从ELKI获取了实现并将它们修改为4-ary堆。获得正确的索引是一个小技巧,因为许多围绕4-ary堆的出版物使用1索引数组的公式?!
以下是基于ELKI单元测试的一些早期基准测试结果。预先分配了200000个Double
个对象(以避免过多地测量内存管理)并进行随机播放。
作为预热,每个堆执行10次迭代,对100次迭代进行基准测试,但我可能会尝试进一步扩展。 10-30秒对于基准测试来说并不是那么明显,而OTOH我也应该尝试测量标准偏差。 在每次迭代中,将200000个元素添加到堆中,然后再次对其中一半元素进行轮询。是的,工作量也可能变得更加复杂。
结果如下:
DoubleMinHeap
:10.371 DoubleMinHeap
:12.356 Heap<Double>
:37.458 PriorityQueue<Double>
:45.875 因此,4-ary堆(可能还没有L1缓存对齐!)和 primitive 的ELKI堆之间的差异不会太大。好吧,10%-20%左右;可能会更糟。
原始double
的堆与Double
个对象的堆之间的差异要大得多。并且ELKI Heap
确实比Java PriorityQueue
明显更快(但那个似乎有很大的变化)。
但是ELKI中有一个轻微的“错误” - 至少原始堆还没有使用批量加载代码。它就在那里,它只是没有被使用,因为每个元素立即修复堆而不是将其延迟到下一个poll()
。我为实验修正了这个问题,主要是通过删除几行并添加一个ensureValid();
调用。此外,我还没有4-ary对象堆,而且我还没有包括ELKI的DoubleObjectMinHeap
但是......相当多的基准测试,我可能会给卡尺一个尝试。
答案 1 :(得分:1)
我自己并没有对此进行基准测试,但有几点可以证明它是相关的。
首先,请注意PriorityQueue的标准Java实现使用二进制堆:
尽管n-a堆的缓存局部性有益,但二进制堆仍然是平均的最佳解决方案,这似乎是合情合理的。以下是为什么会出现这种情况的一些略带手工的原因:
显然你当然需要对自己的数据做自己的基准测试,然后才能得出真正的结论,看看它是否表现最佳(如果差异足以让人关心,我个人怀疑...... 。)
修改强>
另外,我确实使用原始密钥数组编写了一个优先级堆实现,这可能是令人感兴趣的,因为原始海报在下面的评论中提到了原始密钥:
如果有人对运行测试感兴趣,可能会相对容易地将其破解为n-ary版本以进行基准测试。
答案 2 :(得分:1)
我还没有对4-ary堆进行基准测试。我目前正在尝试优化我们自己的堆实现,而且我也在尝试4-ary堆。你是对的:我们需要仔细地对此进行基准测试,因为很容易因实施差异而误导,而热点优化会严重影响结果。另外,小堆可能会显示出与大堆不同的性能特征。
Java PriorityQueue
是一个非常简单的简单堆实现,但这意味着Hotspot会很好地优化它。它一点都不差:大多数人会实现更糟糕的堆。但是,例如,它确实没有进行有效的批量加载或批量添加(批量修复)。然而,在我的实验中,即使在重复插入的模拟中,也很难始终如一地击败这种实现,除非你去寻找真正的大堆。此外,在许多情况下,更换堆中的顶部元素而不是poll()
+ add()
是值得的; java&#39; s PriorityQueue
不支持此功能。
ELKI(以及我认为您是ELKI用户)在某些版本中的性能提升实际上是由于改进了堆实现。但它起伏不定,很难预测哪种堆变化在实际工作负载中表现最佳。我们实施的主要好处可能是有一个&#34; replaceTopElement&#34;功能。你可以在这里检查代码:
SVN de.lmu.ifi.dbs.elki.utilities.heap package
你会注意到我们在那里有一整套集堆。它们针对不同的东西进行了优化,需要更多的重构。其中许多类实际上是从模板生成的,类似于GNU Trove所做的。原因是Java在管理盒装基元时可能会非常昂贵,因此拥有原始版本确实有所回报。 (是的,有计划将其拆分为一个单独的库。它只是没有高优先级。)
请注意,ELKI故意不认可java.util.Collections
API。我们特别发现java.util.Iterator
课程成本很高,因此鼓励人们在整个ELKI中使用C++-style iterators:
for (Iter iter = ids.iter(); iter.valid(); iter.advance()) {
通常会在java.util.Iterator
API上节省大量不必要的对象创建。另外,这些迭代器可以有多个(和原始)值getter;其中Iterator.next()
是吸气剂和高级操作符的混合物。
好的,我现在已经过多了,回到4-ary堆的主题:
如果您打算尝试4-ary堆,我建议您从那里的ObjectHeap
课开始。
更新:我一直是微基准测试,但目前的结果尚无定论。一贯难以击败PriorityQueue
。特别是批量加载和批量修复似乎没有削减我的基准测试中的任何内容 - 可能它们会导致HotSpot优化更少,或者在某些时候进行去优化。通常,更简单的Java代码比复杂逻辑更快。到目前为止,没有批量加载的4-ary堆似乎效果最好。我还没有尝试过5-ary。 3-ary大约相当于4-ary堆;并且4-ary的内存布局更好一些。我也在考虑尝试堆积堆安全阵列大小调整的方法。但我预计代码复杂性的增加意味着它在实践中会运行得更慢。