哈希表与平衡二叉树[关闭]
Posted
技术标签:
【中文标题】哈希表与平衡二叉树[关闭]【英文标题】:Hash table vs Balanced binary tree [closed] 【发布时间】:2011-06-18 07:16:35 【问题描述】:当我需要在哈希表或平衡二叉树之间进行选择以实现集合或关联数组时,我应该考虑哪些因素?
【问题讨论】:
***.com/questions/4128546/… 【参考方案1】:我担心这个问题一般来说无法回答。
问题在于哈希表和平衡二叉树的类型很多,性能差异很大。
所以,天真的答案是:这取决于您需要的功能。如果您不需要排序,请使用哈希表,否则使用平衡二叉树。
要获得更详细的答案,让我们考虑一些替代方案。
Hash Table(请参阅***的条目了解一些基础知识)
并非所有哈希表都使用链表作为存储桶。一种流行的替代方法是使用“更好”的存储桶,例如二叉树或另一个哈希表(带有另一个哈希函数),... 一些哈希表根本不使用存储桶:请参阅开放寻址(显然,它们还有其他问题) 有一种称为线性重新散列(它是实现细节的质量)的东西,它避免了“停止世界并重新散列”的陷阱。基本上在迁移阶段,您只需插入“新”表,并将一个“旧”条目移动到“新”表中。当然,迁移阶段意味着双重查找等......二叉树
重新平衡的成本很高,您可以考虑使用 Skip-List(对于多线程访问也更好)或 Splay Tree。 一个好的分配器可以将节点“打包”到内存中(更好的缓存行为),尽管这并不能缓解指针查找问题。 B-Tree 及其变体还提供“打包”功能我们不要忘记 O(1) 是渐近复杂度。对于少数元素,系数通常更重要(性能方面)。如果您的哈希函数很慢,则尤其如此......
最后,对于集合,您可能还希望考虑概率数据结构,例如Bloom Filters。
【讨论】:
@ProfVersaggi:实际上,这甚至不是真的。一些哈希表处理重复的很差,但有些处理得很好。我建议您阅读 Joaquín M López Muñoz 的 entries on the topic。他创作并维护着 Boost MultiIndex。【参考方案2】:如果不需要以任何顺序保存数据,哈希表通常会更好。如果数据必须保持排序,二叉树会更好。
【讨论】:
虽然不维护排序,但可以维护(插入)顺序的哈希表有些微不足道。 这并不容易。我害怕几件事: 1. 在最坏的情况下,哈希表的性能很差 (O(n)) 2. 为了调整哈希表的大小,我必须重新哈希任何东西,这非常昂贵。这个问题是要知道如何避免这些问题,并了解我缺少的其他问题。 pst:几乎所有“黑盒”集合都可以维护插入顺序;与使用“黑盒”相比,使用哈希表可以在多大程度上更好地维护排序顺序? @peoro:O(n) 实际上是不可能的,除非有人知道你的实现细节并且只是想破坏你。即使考虑调整大小操作(以合理的间隔发生),哈希成本也远低于平衡树。 @peoro:为了放大 Gene 的观点,如果每次调整哈希表的大小都会翻倍(非常典型),那么在调整大小后,一半的项目将被重新散列一次,其中四分之一两次,八分之一三次,四次的 1/16,等等。所以无论表格有多大,平均重复次数都将少于两次。不过,关于退化散列情况的观点是一个很好的观点。例如,如果一个结构类型的 Dictionary 没有覆盖 GetHashCode,并且许多键在第一个字段中具有相同的值,则性能会很差。【参考方案3】:现代架构的一个重要点:如果哈希表的负载因子较低,则通常比二叉树具有更少的内存读取。由于与消耗 CPU 周期相比,内存访问的成本往往相当高,因此哈希表通常更快。
在下面假设二叉树是自平衡的,如红黑树、AVL树或类似treap。
另一方面,如果您决定扩展哈希表时需要重新哈希表中的所有内容,这可能是一项代价高昂的操作(已摊销)。二叉树没有这个限制。
二叉树在纯函数式语言中更容易实现。
二叉树有一个自然的排序顺序和一个自然的方式来遍历所有元素的树。
当哈希表中的负载因子较低时,您可能会浪费大量内存空间,但使用两个指针时,二叉树往往会占用更多空间。
哈希表几乎是 O(1)(取决于你如何处理负载因子)与 Bin 树 O(lg n)。
树木往往是“平均表现者”。没有什么他们做得特别好,但没有什么他们做得特别差。
【讨论】:
【参考方案4】:哈希表查找速度更快:
您需要一个能够生成均匀分布的键(否则您会遗漏很多东西并且不得不依赖哈希以外的其他东西;例如线性搜索)。 哈希可以使用大量的空白空间。您可以保留 256 个条目,但只需要 8 个(到目前为止)。二叉树:
确定性。 O(log n) 我认为... 不需要像哈希表那样的额外空间 必须保持排序。在中间添加一个元素意味着移动其余部分。【讨论】:
你说二叉树是确定性的是什么意思?哈希表也是确定性的。此外,对二叉树的操作是 O(h),其中 h 是高度。如果是平衡二叉树,则h=O(log(n))。 不是真的!哈希表可能会“错过”。例如,如果您有一个 10 的数组并使用电话号码对其进行索引(例如使用模数),您可以获得指向数组第一个元素的哈希值。但是,如果在构建数组时首先使用 9 个具有相同哈希的其他数字;你实际上必须一直走到最后一个元素。在二进制搜索中,无论如何你都可以保证得到 BigO(log n)。 !免责声明!这完全取决于您如何建立哈希排序/搜索。有很多方法... 在中间添加一个元素并不意味着移动其余的元素。它是一个链接的数据结构,而不是一个数组(也许你把二叉搜索树和二叉搜索混淆了,这是两个非常不同的东西。所有操作都是 O(log(n)),如果添加/删除到中间意味着移动其余部分本来是 O(n)。 这完全取决于你如何实现它......使用链接树是绕过二分搜索插入问题的好方法。然而,二分查找(下面是否有一棵树)总是会返回 O(log n) 的结果。除非输入键与生成的哈希是 1:1,否则哈希不能。【参考方案5】:二叉搜索树需要键之间的全序关系。哈希表只需要具有一致哈希函数的等价或身份关系。
如果全序关系可用,则排序数组具有与二叉树相当的查找性能、哈希表顺序的最差插入性能,以及比两者都更小的复杂性和内存使用。
如果可以接受增加最坏情况查找复杂度,则哈希表的最坏情况插入复杂度可以保持在 O(1)/O(log K)(K 是具有相同哈希值的元素的数量)如果可以对元素进行排序,则为 O(K) 或 O(log K)。
如果键发生变化,树和哈希表的不变量的恢复成本很高,但对于排序数组来说则小于 O(n log N)。
在决定使用哪种实现时需要考虑以下因素:
-
总订单关系的可用性。
为等价关系提供良好的散列函数。
元素数量的先验知识。
了解插入、删除和查找的速率。
比较和散列函数的相对复杂性。
【讨论】:
"二叉搜索树需要键之间的全序关系。哈希表只需要具有一致哈希函数的等价或身份关系。"这是误导。二叉搜索树总是可以使用与哈希表相同的键:哈希值。与哈希表相比,它对可以使用树的情况没有限制。 @rlibby 尽管默认情况下散列键的大多数实现都使用定义了总顺序的类型(整数或指针),但如果您提供自己的散列,则只需要等价。所以,一般来说,你不能在哈希键上使用二叉搜索树,因为你不知道哈希是什么,它们来自哪里,或者如果它们支持全序关系,那就更不用说了。 但如果我正确理解您的建议,那么这样的哈希值也不能在哈希表中使用。当然,如果它可以用在哈希表中,那么它也可以用在树集中。如果它可以在表中使用,那么它必须映射到表中的某个索引。可以使用生成此索引的函数来生成树集的键。 @rlibby 哈希表要求相等的元素具有相同的哈希值,但不要求不同的元素具有不同的哈希值。如果不同的元素具有相同的哈希,则不存在全序关系。 如果不同的元素经常具有相同的哈希值,那么哈希表无论如何都会非常慢。确实可以在二叉树的每个节点上存储一个链表,就像在哈希表的每个条目上存储一个链表一样。【参考方案6】:如果您只需要访问单个元素,哈希表会更好。如果你需要一系列元素,除了二叉树你别无选择。
【讨论】:
【参考方案7】:要补充上述其他出色的答案,我想说:
如果数据量不会改变(例如存储常量),则使用哈希表;但是,如果数据量会发生变化,请使用树。这是因为,在散列表中,一旦达到负载因子,散列表必须调整大小。调整大小操作可能非常缓慢。
【讨论】:
由于调整大小,将元素添加到哈希表的最坏情况时间是 O(n),但如果哈希表每次大小翻倍,则需要重新哈希的添加部分将随着表大小的增加而下降。无论表有多大,每个元素的平均重新哈希操作次数永远不会超过两次。 如果哈希表大小是 加倍,那么如果冲突数量减少,我会感到惊讶,因为哈希表在桌子的大小是素数。此外,如果您要求系统在每次调整大小时为您提供两倍的内存,那么您将很快耗尽内存(或者如果系统重新排列内存以提供您所需的连续内存量,则会减慢系统速度'要求)。 加倍是一种常见的策略,但不是必需的。需要的是指数增长。如果你愿意,你可以选择一个更小的指数,这只是意味着重新哈希操作的平均次数会更高。在任何情况下,指数增长的表中 n 次插入的摊销成本为 O(n),而自平衡二叉搜索树的成本为 O(n*log(n))。【参考方案8】:我认为尚未解决的一点是树对于持久数据结构来说要好得多。也就是说,不可变的结构。如果不修改整个表,就无法修改标准哈希表(即使用单个链表数组的哈希表)。与此相关的一种情况是,如果两个并发函数都具有哈希表的副本,并且其中一个更改了该表(如果该表是可变的,则该更改对另一个也可见)。另一种情况是这样的:
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) 个节点(树的其余部分相同)。
这意味着当需要不可变映射时,高效的树会非常方便。
【讨论】:
【参考方案9】:如果您有许多略有不同的集合实例,您可能希望它们共享结构。这对树很容易(如果它们是不可变的或写时复制)。我不确定你可以用哈希表做多好;至少不那么明显。
【讨论】:
【参考方案10】:根据我的经验,hastables 总是更快,因为树受到太多缓存影响。
要查看一些真实数据,您可以查看我的 TommyDS 库的基准页面http://tommyds.sourceforge.net/
您可以在这里看到最常见的哈希表、树和树库的性能比较。
【讨论】:
【参考方案11】:需要注意的一点是关于遍历、最小和最大项。哈希表不支持任何类型的有序遍历,也不支持访问最小或最大项目。如果这些能力很重要,二叉树是更好的选择。
【讨论】:
以上是关于哈希表与平衡二叉树[关闭]的主要内容,如果未能解决你的问题,请参考以下文章