哈希表与平衡二叉树

时间:2011-01-31 00:00:28

标签: algorithm language-agnostic data-structures hash tree

当我需要在哈希表或平衡二叉树之间进行选择以实现集合或关联数组时,我应该考虑哪些因素?

11 个答案:

答案 0 :(得分:51)

这个问题无法回答,总的来说,我担心。

问题在于有很多类型的哈希表和平衡二叉树,它们的性能差异很大。

所以,天真的答案是:它取决于你需要的功能。如果您不需要排序则使用哈希表,否则使用平衡二叉树。

有关更详细的答案,让我们考虑一些替代方案。

Hash Table(参见维基百科的一些基础知识)

  • 并非所有哈希表都使用链表作为存储桶。一种流行的替代方法是使用“更好”的存储桶,例如二叉树或其他散列表(使用另一个散列函数),...
  • 有些哈希表根本不使用存储桶:请参阅打开寻址(显然它们带有其他问题)
  • 有一种称为线性重新散列的东西(它是一种实现细节的质量),它避免了“停止世界和重新散列”的陷阱。基本上在迁移阶段,您只需插入“新”表,并将一个“旧”条目移动到“新”表中。当然,迁移阶段意味着双重查找......

二叉树

  • 重新平衡成本很高,你可以考虑一个Skip-List(对于多线程访问也更好)或一个Splay Tree。
  • 一个好的分配器可以将节点“打包”在内存中(更好的缓存行为),即使这不会缓解指针查找问题。
  • B-Tree和变体也提供“打包”

我们不要忘记O(1)是渐近的复杂性。对于少数元素,系数通常更重要(性能方面)。如果您的哈希函数很慢,则尤其如此......

最后,对于集合,您可能还希望考虑概率数据结构,例如Bloom Filters

答案 1 :(得分:41)

如果不需要以任何顺序保存数据,散列表通常会更好。如果必须对数据进行排序,二叉树会更好。

答案 2 :(得分:11)

现代架构上值得注意的一点:如果Hash表的加载因子较低,则通常会比二叉树具有更少的内存读取。由于与刻录CPU周期相比,内存访问的成本相当高,因此哈希表通常更快。

在下面的二叉树中,假设是自平衡的,如红黑树,AVL树或类似treap

另一方面,如果您在决定扩展时需要重新散列哈希表中的所有内容,这可能是一个代价高昂的操作(摊销)。二叉树没有这个限制。

二进制树在纯函数式语言中更容易实现。

二叉树具有自然的排序顺序和自然的方式来遍历所有元素的树。

当哈希表中的加载因子很低时,你可能会浪费大量的内存空间,但是有两个指针,二叉树往往会占用更多的空间。

哈希表几乎是O(1)(取决于你如何处理负载因子)与Bin树O(lg n)。

树木往往是“平均表现者”。他们没有什么特别好的,但是他们做的并不是特别糟糕。

答案 3 :(得分:7)

二叉搜索树需要密钥之间的总顺序关系。哈希表仅需要具有一致哈希函数的等价或身份关系。

如果总订单关系可用,则排序数组的查找性能可与二进制树相媲美,最差情况下的插入性能按哈希表的顺序排列,复杂性和内存使用量均低于二者。

如果可以接受增加最坏情况查找复杂度,则哈希表的最坏情况插入复杂度可以留在O(1)/ O(log K)(K具有相同散列的元素数)如果可以对元素进行排序,则为O(K)或O(log K)。

如果密钥更改,则树和哈希表的不变量都很难恢复,但是对于已排序的数组,要小于O(n log N)。

这些是在决定使用哪种实施时需要考虑的因素:

  1. 总订单关系的可用性。
  2. 为等价关系提供良好的散列函数。
  3. 关于元素数量的A-priory知识。
  4. 了解插入,删除和查找的速度。
  5. 比较和散列函数的相对复杂性。

答案 4 :(得分:6)

哈希表的查找速度更快:

  • 你需要一个生成均匀分布的密钥(否则你会错过很多东西并且必须依赖哈希以外的东西;比如线性搜索)。
  • 哈希可以使用很多空白空间。您可以保留256个条目,但只需要8个(到目前为止)。

二叉树:

  • 确定性。 O(log n)我认为......
  • 不需要像哈希表这样的额外空间
  • 必须保持分类。在中间添加元素意味着移动其余部分。

答案 5 :(得分:3)

如果您只需要访问单个元素,哈希表就更好了。如果您需要一系列元素,那么除了二叉树之外别无选择。

答案 6 :(得分:3)

要添加上面的其他重要答案,我会说:

如果数据量不会改变(例如存储常量),则使用哈希表;但是,如果数据量会发生变化,请使用树。这是因为在哈希表中,一旦达到加载因子,哈希表必须调整大小。调整大小操作可能非常慢。

答案 7 :(得分:2)

我认为没有解决的一点是,树对于持久性数据结构来说要好得多。也就是说,不可变的结构。如果不修改整个表,则无法修改标准哈希表(即使用单个链表列表的哈希表)。与此相关的一种情况是,如果两个并发函数都具有哈希表的副本,并且其中一个更改了表(如果该表是可变的,则该更改对于另一个也是可见的)。另一种情况如下:

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 )节点(树的其余部分相同)。

这意味着当需要不可变映射时,高效的树可以非常方便。

答案 8 :(得分:1)

如果您有许多稍微不同的集合实例,您可能希望它们共享结构。对于树来说这很容易(如果它们是不可变的或写入时复制)。我不确定你能用哈希表做得多好;它至少不那么明显了。

答案 9 :(得分:1)

根据我的经验,hastables总是更快,因为树遭受了太多的缓存效应。

要查看一些真实数据,您可以查看我的TommyDS库的基准页面http://tommyds.sourceforge.net/

在这里,您可以看到比较最常见的散列表,树和可用库的性能。

答案 10 :(得分:0)

需要注意的一点是关于遍历,最小和最大项目。哈希表不支持任何类型的有序遍历,也不支持访问最小或最大项目。如果这些功能很重要,那么二叉树是更好的选择。