将哈希表与二叉搜索树进行比较

15
我们都知道,如果散列函数被很好地选择,则哈希表对于插入和查找都具有O(1)的时间复杂度。那么,我们为什么要使用二叉搜索树呢?只是因为设计完美散列函数很难吗?
我提出这个问题的原因是:我注意到标准C ++ STL具有用二叉搜索树实现的set和map,但没有哈希(不是指非标准的hash_set,hash_map)。而Ruby只有Hash。我想了解这种差异背后的原理。

可能是二叉树 vs. 链表 vs. 哈希表的重复问题。 - Ciro Santilli OurBigBook.com
8个回答

25

树允许按顺序遍历。

哈希表的最坏情况性能为O(N)(在一个桶中进行线性搜索),二叉搜索的复杂度为O(log N)。

注意:这需要树保持平衡,这就是为什么典型实现使用自平衡树(例如红黑树)的原因。

虽然这种退化情况比较少见,但不是不可能出现,而且它很大程度上取决于选择合适的哈希函数和实际数据的分布情况。

树的实现也很容易扩展到所需大小,而哈希表在填满一定比例(对于大多数实现来说是约70%的桶被填满)后性能开始下降。你需要重新对整个表进行哈希(对于实时应用程序来说是糟糕的),或者逐步移动到新的表格,这并不是一个简单的实现。

最终,STL 可能只选择了一种“基本”容器模板,即树,以避免额外的实现复杂性。


1
一个二叉树可以是100%不平衡的,这意味着它采取了链表的形式。这意味着它的最坏情况性能为*O(n)*。 - Björn Lindqvist
1
@BjörnLindqvist:没错 - 这就是为什么基于树的容器通常使用自平衡树,比如红黑树(https://en.wikipedia.org/wiki/Red%E2%80%93black_tree)的原因。 - peterchen

9

补充peterchen的答案,哈希结构虽然理论上插入和删除速度更快,但实际上取决于数据本身、选择的哈希函数以及数据量。

  • 完美的哈希函数取决于数据的数量和分布。

最佳和最差情况之间的性能差异很大,使它们不适用于通用结构。另一方面,二叉树更加可预测,独立于使用的数据量/类型,尽管在最佳情况下效率较低。


6
STL最初并没有将哈希表作为容器之一,因为哈希表更加复杂 - 您必须在开放和关闭寻址之间做出选择,更不用说哈希函数等。当时,Stepanov和Stroustrup试图加快其进展,以便快速被标准接受。
另一方面,树比较简单。已经知道由于这些是内存数据结构,我们可以使用二叉树而不是B树。然后在AVL和RB树之间进行选择。RB树往往会由于更好的性能特征而被选择,我无法评论,但是维基百科对两种结构(AVLRB)的文章会告诉您更多信息。
否则,树和哈希表适用于不同的场景。如果您需要快速插入或检索,并且不关心它们存储的顺序,则可以使用哈希表。如果您需要排序特性和插入和检索的强保证,则可以使用二叉树。另一个好的经验法则是进行性能分析。由于大多数使用都与接口兼容,因此进行性能分析以查看哪个提供更好的性能也很有帮助。

3
您可以按顺序访问二叉搜索树中的数据。

1
如果可以做到,你应该总是优先选择哈希表而不是二叉搜索树。哈希表的内存开销比树高,但它们使用的所有内存可以分配在一个大块中。对于树来说,每个添加的节点都需要单独的分配,这会导致高度碎片化并且对性能不利。类似于您更喜欢从1个文件中读取1000个字节而不是从1000个不同的文件中读取1个字节的情况。
哈希表无法解决的情况是有序的情况。例如,假设您正在编写一个内存分配器,并且将空闲内存块存储在数据结构中。键是块的大小,值是指向它们的指针。
请求内存涉及浏览此数据结构并找到满足请求的最小(意味着排序!)块。例如,如果您具有键为10、20、30的块,并且收到20字节内存的请求,则选择第二个块。哈希映射可以轻松完成这项工作。

但是,如果请求的是22个字节呢?由于没有值为20的键,您必须迭代整个哈希映射以找到正确的键(30),这是一个O(n)操作。但是,如果您使用了一棵树,则“查找大于给定键的最小键”是一个O(log n)操作。


1

嗯,搜索树是有序的,哈希表则不是。


这似乎只有在遍历它时才有影响。 - pierrotlefou

1
使用一棵树需要对树中的项进行排序。使用哈希表需要一个函数来计算哈希表中项的哈希值。
有趣的是,.NET框架要求每个类都实现(或继承)GetHashCode函数,使得每个对象都能存储在哈希表中。然而,这也给开发人员增加了额外的负担,即使他们不打算将该类用作哈希表,也需要实现语义正确的哈希函数。一种解决方案是从GetHashCode返回一个常量值,这在语义上是正确的,但如果该函数被用于哈希,效率就不太高。

0

在 C++ 时代,人们仍然喜欢硬核学术方法来处理数据结构和算法,因此他们更喜欢占用更少内存并且最好和最坏情况都能够被很好理解的数据结构。

当 Ruby 出现时,为了脚本编程的目的,人们意识到他们更喜欢简单易懂而不是原始性能。由于哈希表允许使用顺序索引作为键来实现数组的语义,同时也可以使用自然键来实现字典的语义,因此它们被认为是更通用的数据结构。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接