【发布时间】:2011-06-18 07:16:35
【问题描述】:
当我需要在哈希表或平衡二叉树之间进行选择以实现集合或关联数组时,我应该考虑哪些因素?
【问题讨论】:
标签: algorithm language-agnostic data-structures hash tree
当我需要在哈希表或平衡二叉树之间进行选择以实现集合或关联数组时,我应该考虑哪些因素?
【问题讨论】:
标签: algorithm language-agnostic data-structures hash tree
我担心这个问题一般来说无法回答。
问题在于哈希表和平衡二叉树的类型很多,性能差异很大。
所以,天真的答案是:这取决于您需要的功能。如果您不需要排序,请使用哈希表,否则使用平衡二叉树。
要获得更详细的答案,让我们考虑一些替代方案。
Hash Table(请参阅维基百科的条目了解一些基础知识)
二叉树
我们不要忘记 O(1) 是渐近复杂度。对于少数元素,系数通常更重要(性能方面)。如果您的哈希函数很慢,则尤其如此......
最后,对于集合,您可能还希望考虑概率数据结构,例如Bloom Filters。
【讨论】:
如果不需要以任何顺序保存数据,哈希表通常会更好。如果数据必须保持排序,二叉树会更好。
【讨论】:
现代架构的一个重要点:如果哈希表的负载因子较低,则通常比二叉树具有更少的内存读取。由于与消耗 CPU 周期相比,内存访问的成本往往相当高,因此哈希表通常更快。
在下面假设二叉树是自平衡的,如红黑树、AVL树或类似treap。
另一方面,如果您决定扩展哈希表时需要重新哈希表中的所有内容,这可能是一项代价高昂的操作(已摊销)。二叉树没有这个限制。
二叉树在纯函数式语言中更容易实现。
二叉树有一个自然的排序顺序和一个自然的方式来遍历所有元素的树。
当哈希表中的负载因子较低时,您可能会浪费大量内存空间,但使用两个指针时,二叉树往往会占用更多空间。
哈希表几乎是 O(1)(取决于你如何处理负载因子)与 Bin 树 O(lg n)。
树木往往是“平均表现者”。没有什么他们做得特别好,但没有什么他们做得特别差。
【讨论】:
哈希表查找速度更快:
二叉树:
【讨论】:
二叉搜索树需要键之间的全序关系。哈希表只需要具有一致哈希函数的等价或身份关系。
如果全序关系可用,则排序数组具有与二叉树相当的查找性能、哈希表顺序的最差插入性能,以及比两者都更小的复杂性和内存使用。
如果可以接受增加最坏情况查找复杂度,则哈希表的最坏情况插入复杂度可以保持在 O(1)/O(log K)(K 是具有相同哈希值的元素的数量)如果可以对元素进行排序,则为 O(K) 或 O(log K)。
如果键发生变化,树和哈希表的不变量的恢复成本很高,但对于排序数组来说则小于 O(n log N)。
在决定使用哪种实现时需要考虑以下因素:
【讨论】:
如果您只需要访问单个元素,哈希表会更好。如果你需要一系列元素,除了二叉树你别无选择。
【讨论】:
要补充上述其他出色的答案,我想说:
如果数据量不会改变(例如存储常量),则使用哈希表;但是,如果数据量会发生变化,请使用树。这是因为,在散列表中,一旦达到负载因子,散列表必须调整大小。调整大小操作可能非常缓慢。
【讨论】:
我认为尚未解决的一点是树对于持久数据结构来说要好得多。也就是说,不可变的结构。如果不修改整个表,就无法修改标准哈希表(即使用单个链表数组的哈希表)。与此相关的一种情况是,如果两个并发函数都具有哈希表的副本,并且其中一个更改了该表(如果该表是可变的,则该更改对另一个也可见)。另一种情况是这样的:
def bar(table):
# some intern stuck this line of code in
table["hello"] = "world"
return table["the answer"]
def foo(x, y, table):
z = bar(table)
if "hello" in table:
raise Exception("failed catastrophically!")
return x + y + z
important_result = foo(1, 2, {
"the answer": 5,
"this table": "doesn't contain hello",
"so it should": "be ok"
})
# catastrophic failure occurs
对于可变表,我们不能保证函数调用接收到的表在整个执行过程中都会保持不变,因为其他函数调用可能会修改它。
因此,可变性有时并不是一件令人愉快的事情。现在,解决这个问题的一种方法是保持表不可变,并让更新返回一个 new 表而不修改旧表。但是对于哈希表,这通常是一个代价高昂的 O(n) 操作,因为需要复制整个底层数组。另一方面,使用平衡树,可以生成一棵新树,只需要创建 O(log n) 个节点(树的其余部分相同)。
这意味着当需要不可变映射时,高效的树会非常方便。
【讨论】:
如果您有许多略有不同的集合实例,您可能希望它们共享结构。这对树很容易(如果它们是不可变的或写时复制)。我不确定你可以用哈希表做多好;至少不那么明显。
【讨论】:
根据我的经验,hastables 总是更快,因为树受到太多缓存影响。
要查看一些真实数据,您可以查看我的 TommyDS 库的基准页面http://tommyds.sourceforge.net/
您可以在这里看到最常见的哈希表、树和树库的性能比较。
【讨论】:
需要注意的一点是关于遍历、最小和最大项。哈希表不支持任何类型的有序遍历,也不支持访问最小或最大项目。如果这些能力很重要,二叉树是更好的选择。
【讨论】: