我们都知道,如果很好地选择了哈希函数,哈希表对插入和查找都有O(1)时间。那么,我们想要使用二进制搜索树的原因是什么?仅仅因为完美的哈希函数难以设计?
我在这里如何提出这个问题?
我注意到标准 C ++ STL有set
和map
,它们是用二进制搜索树实现的,但没有哈希(不是谈论非标准hash_set
, hash_map
)。虽然,Ruby只有Hash
。我想了解这种差异背后的理性。
答案 0 :(得分:24)
树允许有序转换。
哈希表的最坏情况性能是O(N)(通过一个桶进行线性搜索),二进制搜索受O(log N)约束。
注意:这需要树平衡 - 这就是为什么典型的实现使用自平衡树,suhc作为红黑树。
虽然这种降级不太可能,但这并非不可能,并且很大程度上取决于能力来选择适当的散列函数和实际数据的分布。
树实现也会平滑地增长到所需的大小,而hashmap在它满了时开始降级(对于大多数实现,它表示大约70%的桶已填满)。您需要重新整理整个表格(再次,实时应用程序不好),或者逐步移动到新表格,这不是一个简单的实现。
最后,STL可能只使用了一个“基础”容器模板树,以避免额外的实现复杂性。
答案 1 :(得分:9)
要添加peterchen答案,哈希结构虽然理论上插入和删除速度更快,但在很大程度上取决于实际数据,选择的哈希函数和数据量。
在最佳和最差情况之间存在较大的性能差异,使其不适合通用结构。另一方面,二进制树更容易预测,与所使用的数据量/类型无关,即使在最佳情况下效率较低。
答案 2 :(得分:6)
STL最初没有在容器中包含哈希表,因为哈希表更复杂 - 您必须在开放和封闭寻址之间进行选择,更不用说哈希函数等。当时,Stepanov和Stroustrup是试图加快进度,以便迅速被标准接受。
另一方面,树木相对简单。众所周知,由于这些是内存中的数据结构,我们可以使用二叉树而不是B树。然后它是AVL和RB树之间的选择。由于更好的性能特征,我倾向于选择RB树,我无法评论,但维基百科关于这两种结构(AVL和RB)的文章将以相对较好的细节告诉你更多否则,树和哈希表适用于不同的东西。如果您需要快速插入或检索,并且无关心它们存储的顺序,则哈希表是好的。如果您需要对插入和检索进行排序特性和强有力的保证,那么二叉树就很好了。另一个好的经验法则是剖析。由于其中任何一种的大多数用途都是接口兼容的,因此查看哪些可以提供更好的性能也有帮助。
答案 3 :(得分:3)
您可以按顺序访问二叉搜索树中的数据。
答案 4 :(得分:1)
井搜索树是有序的,哈希不是。
答案 5 :(得分:1)
要使用树,您需要一种方法来订购树中的项目。要使用哈希表,您需要一个函数来计算哈希表中项的哈希值。
有趣的是,.NET框架要求每个类实现(或继承)GetHashCode
函数,使每个对象都存储在哈希表中。但是,这也为需要实现语义正确的哈希函数的开发人员增加了额外的负担,即使他们不打算对类进行哈希处理。一种解决方案是从GetHashCode
返回一个恒定值,这在语义上是正确的,但如果该函数用于散列则效率不高。
答案 6 :(得分:1)
如果你能逃脱它,你应该总是喜欢二进制搜索树上的哈希。散列具有比树更高的内存开销,但它们使用的所有内存都可以分配在一个大块中。对于树,添加的每个节点都需要单独的分配,这会导致高碎片并且对性能不利。类似于您更喜欢从1个文件读取1000个字节而不是1000个不同文件中的1个字节。
哈希不起作用的情况是在订购事项时。例如,假设您正在编写内存分配器,并在数据结构中存储空闲的内存块。键是块的大小,值是指向它们的指针。
对内存的请求需要查看此数据结构并找到满足请求的最小(隐含排序!)块。例如,如果您有包含键10,20,30的块并且有20个字节的内存请求,则选择第二个块。 hashmap可以轻松完成。
但是如果请求是22字节怎么办?由于没有值为20的键,因此必须迭代整个hashmap以找到正确的键(30),这是一个O(n)操作。但是如果你使用了一棵树,那么找到比给定密钥大的最小密钥"是一个O(log n)操作。
答案 7 :(得分:0)
在C ++时代,人们仍然是数据结构和算法的核心学术方法的粉丝,因此他们更喜欢具有较小内存占用和易于理解的最佳和最差情况行为的结构。
当Ruby出现时,为了编写脚本,人们意识到他们喜欢简单而不是原始性能,并且因为hashtables允许两个数组的语义(如果你使用顺序索引作为键)和字典(如果你使用)它们被认为是更普遍的数据结构。