二叉树vs链表vs哈希表


72

我正在为我正在从事的项目构建符号表。我想知道人们对于存储和创建符号表的各种方法的优缺点有何看法。

我已经做了相当多的搜索,最常用的建议是二叉树或链表或哈希表。以上所有优点和缺点是什么?(在C ++中工作)


Answers:


48

您的用例大概将是“一次插入数据(例如,应用程序启动),然后执行大量读取,但如果有任何额外的插入,则很少”。

因此,您需要使用一种快速查找所需信息的算法。

因此,我认为HashTable是最适合使用的算法,因为它只是生成关键对象的哈希,然后使用该哈希来访问目标数据-它是O(1)。其他的是O(N)(大小为N的链接列表-您必须一次遍历一个列表,平均重复N / 2次)和O(log N)(二叉树-您将搜索空间减半)每次迭代-仅在树是平衡的情况下,因此这取决于您的实现,不平衡的树的性能可能会大大降低)。

只需确保HashTable中有足够的空间(存储桶)来存储您的数据(Re,Soraz在此岗位上的评论)。大多数框架实现(Java,.NET等)的质量都不需要担心实现。

您是否在大学学习过有关数据结构和算法的课程?


43
还没有离开高中...所以不。全部自学:)
benmcredmond

6
仅当存储桶的数量占总数的一小部分时,哈希表查找的O(1)才适用。也就是说,如果您要在512个存储桶中存储100万个条目,那么您仍将进行2048个直接比较pr查找,这比100万的log(n)(或13个直接比较pr查找)
Soraz

3
哈希表的质量实现以及质量哈希算法将得到O(1)。二叉树的错误实现也可能比O(log N)更糟。因此,对于所问的问题级别,说哈希表为O(1)可能已经足够了。
达伦(Darron)

符号表具有其他属性,这些属性通常使哈希表不是最合适的。-1
斯蒂芬·埃格蒙特

3
@Stephan:详细说明。我声称哈希表是迄今为止用于符号表的最常见的数据结构。
康拉德·鲁道夫

76

这些数据结构之间的标准折衷适用。

  • 二叉树
    • 实现的中等复杂度(假设您无法从库中获取它们)
    • 插入为O(logN)
    • 查找为O(logN)
  • 链接列表(未排序)
    • 实施复杂度低
    • 插入是O(1)
    • 查找为O(N)
  • 哈希表
    • 实施难度大
    • 插入平均为O(1)
    • 查找平均为O(1)

3
对于未排序的链表,插入是O(1)而不是O(N),而在进行双链连接时,将其与O(1)一起移除通常是使用它们的动机,而不是实现复杂性。另一个动机是它们可以无限制地增长,而不会被复制。我并不是在这种情况下建议一个。
P爸爸

4
我还要指出,哈希表与正确平衡的二叉树一样容易实现。但这是非常主观的。
JoelBorggrén-Franck'08

4
是的,实现的复杂性是主观的。但是我认为最小的链表比最小的哈希表更简单。然后添加自动平衡与冲突以及在满时调整大小不会交换订单。
达伦(Darron)

1
二进制树的一个特征是它们允许(键)排序的迭代。
乔纳丹·赫德堡

删除操作呢?
anon58192932 2013年

43

似乎每个人都忘记的是,对于较小的Ns(即表中的符号很少),链表可以比哈希表快得多,尽管从理论上讲,其渐进复杂度确实更高。

从Pike的C语言编程笔记中可以找到一个著名的qoute:“规则3。当n很小时,fancy算法很慢,而n通常很小。fancy算法具有很大的常数。直到您知道n经常变得很大,不要幻想。” http://www.lysator.liu.se/c/pikestyle.html

我无法从您的帖子中得知您是否要处理小N,但是请始终记住,针对大N的最佳算法不一定适合于小N。


2
那取决于实现。如果您偶然知道用于计算哈希值的算法,则可以估算它与n / 2个身份比较(链表的平均值)或log(n)个身份比较(二叉树的平均值)相比将有多昂贵。 。
汉克·盖伊

1
您没有提到您使用的是哪种语言,但是如果它对字典/哈希表/它所调用的语言(例如Python)具有良好的内置支持,那么最简单的就是学会停止担心并喜欢内置的。
汉克·盖伊

正如Hank所写,在不知不知道的情况下,无法猜出有什么大的限制:输入数据集,哈希算法,编程语言(是否插入了字符串)等。通常,您在了解以上所有内容时都会犯错。选择最容易编码的代码,如果速度较慢,请稍后修复。
JoelBorggrén-Franck08年

另外,平均 一个二叉树应该是(log n)/ 2
Hank Gay

2
同样,使用高级算法的“调试怪异错误的时间”要高得多。保持简单,直到简单被证明是站不住脚的。
杰夫·艾伦,

8

听起来以下事实可能都是正确的:

  • 您的密钥是字符串。
  • 插入一次。
  • 查找频繁进行。
  • 键值对的数量相对较小(例如,少于K个左右)。

如果是这样,您可能会考虑这些其他结构中的任何一个排序列表。这在插入期间的性能会比其他方法差,因为插入时排序列表为O(N),而链表或哈希表的排序列表为O(1),而O(log 2N)表示平衡的二叉树。但是排序列表中的查找可能比其他任何结构都要快(我将在稍后对此进行解释),因此您可能会排名第一。另外,如果您一次执行所有插入操作(或者在完成所有插入操作之前不需要查找),则可以简化对O(1)的插入操作,并在最后进行一种更快的排序。而且,排序后的列表使用的内存比其他任何结构都要少,但这唯一可能的影响是如果您有很多小列表。如果您有一个或几个大列表,则哈希表的性能可能会超过排序列表。

为什么使用排序列表查找会更快?好吧,很明显,它比链表的O(N)查找时间要快。对于二叉树,如果树保持完美平衡,则查找仅保持O(log 2 N)。使树保持平衡(例如,红黑色)会增加复杂性和插入时间。此外,对于链表和二叉树,每个元素都是一个单独分配的1 节点,这意味着您必须取消引用指针,并且可能跳转到可能变化很大的内存地址,从而增加了发生高速缓存未命中的机会。

至于哈希表,您可能应该在此处阅读有关StackOverflow的其他几个问题,但这里的主要关注点是:

  • 在最坏的情况下,哈希表可能退化为O(N)。
  • 散列的成本不为零,在某些实现中,这可能很重要,尤其是在字符串的情况下。
  • 就像在链表和二叉树中一样,每个条目都是一个节点,不仅存储键和值,而且在某些实现中也单独分配它们,因此您使用更多的内存并增加了缓存未命中的机会。

当然,如果您真的在乎这些数据结构将如何执行,则应该对其进行测试。对于大多数常见语言,找到其中任何一种的良好实现,应该几乎没有问题。在这些数据结构中的每个数据结构上抛出一些实际数据并查看哪种方法效果最好应该不太困难。

  1. 一个实现有可能预先分配一个节点数组,这将有助于解决缓存丢失问题。尽管您当然可以自己滚动,但在链表或二进制树的任何实际实现中都没有看到这一点(当然,并不是我见过的每一个)。但是,由于节点对象必然大于键/值对,因此仍然可能会出现高速缓存未命中的情况。

对于哈希表(在这种情况下),可以达到O(1),因为您事先知道要在其中哈希的所有数据。因此,我猜想排序数组的唯一优点是空间复杂度。
Iulius Curt

7

我喜欢比尔的答案,但它并不能真正综合事物。

从三个选择中:

链接列表从(O(n))查找项目相对较慢。因此,如果表中有很多项目,或者要进行很多查找,那么它们并不是最佳选择。但是,它们易于构建,也易于编写。如果表很小,并且/或者您在构建表之后只对它进行过一次小扫描,那么这可能是您的选择。

哈希表的速度非常快。但是,要使其正常工作,您必须为输入选择一个良好的哈希值,并且必须选择一个足够大的表以容纳所有内容而不会发生很多哈希冲突。这意味着您必须了解输入内容的大小和数量。如果搞砸了,最终会得到一组非常昂贵和复杂的链表。我想说的是,除非您提前知道表的大小,否则不要使用哈希表。这与您的“接受”答案不同。抱歉。

留下树木。但是,您可以在这里进行选择:平衡还是不平衡。通过在C和Fortran代码上研究此问题,我们发现这里的符号表输入往往具有足够的随机性,以至于由于不平衡树而只会损失一两棵树。鉴于平衡树插入元素的速度较慢且难以实现,因此我不会理会它们。但是,如果您已经可以访问漂亮的调试组件库(例如:C ++的STL),那么您不妨继续使用平衡树。


尽管我确实同意您对HashTables的观点,但我的回答是针对一个非常特定的用例-读取一次,很少添加(如果有的话)并且进行大量读取-因此,假设HashTable的大小正确(自动增长或设置为1.2) x输入大小)是最好的选择。
JeeBee

您事先知道输入大小的情况是非常不寻常的特殊情况。当然,在这种特殊情况下,请使用哈希表。但本没有任何迹象表明他的案子符合这种罕见的条件。
TED

6

需要注意的几件事。

  • 二叉树只有O(log n)查找,并且如果树是平衡的,则插入复杂度。如果您的符号以非常随机的方式插入,这应该不是问题。如果按顺序插入它们,则将建立一个链接列表。(对于您的特定应用程序,它们不应处于任何顺序,因此您应该可以。)如果符号可能太有序,则红黑树是更好的选择。

  • 哈希表提供O(1)平均插入和查找复杂度,但是这里也有一个警告。如果您的哈希函数很糟糕(我的意思是真的很糟糕),那么您最终也可能会在这里建立一个链表。但是,任何合理的字符串哈希函数都应该执行此操作,因此,此警告实际上仅是确保您知道它可能发生。您应该能够测试您的散列函数在预期的输入范围内没有很多冲突,这样就可以了。另一个较小的缺点是,如果您使用的是固定大小的哈希表。大多数哈希表实现会在达到一定大小时增长(更精确地说是负载因子,请参见此处有关详细信息)。这是为了避免在向十个存储桶中插入一百万个符号时遇到的问题。这只会导致十个链接列表,平均大小为100,000。

  • 如果我的符号表很短,我只会使用链表。这是最容易实现的,但是链表的最佳情况性能是其他两个选项的最差情况。


关于1:这是一个好点。过去实现符号表时,通常会发现遇到的输入几乎是随机的(字母顺序)。因此,确实没有足够的回报来使它值得平衡树。
TED

1

其他评论集中在添加/检索元素上,但是,如果不考虑迭代整个集合所需的时间,则本讨论是不完整的。简短的答案是,哈希表需要较少的内存来进行迭代,但是树所需的时间更少。

对于哈希表,遍历(键,值)对的内存开销不取决于表的容量或表中存储的元素数量;实际上,迭代只需要一个或两个索引变量。

对于树,所需的内存量始终取决于树的大小。您可以在迭代时维护未访问节点的队列,也可以添加指向树的其他指针以简化迭代(出于迭代目的,使树像链接列表一样工作),但是无论哪种方式,您都必须为迭代分配额外的内存。

但是,在计时方面,情况恰恰相反。对于哈希表,迭代所需的时间取决于表的容量,而不是存储元素的数量。因此,以10%的容量加载的表进行迭代所花费的时间将比具有相同元素的链表的访问时间长10倍!


0

当然,这取决于几件事。我想说的是,链表是正确的,因为它几乎没有适合用作符号表的属性。如果您已有一棵二叉树,而不必花费时间编写和调试它,那么二叉树可能会起作用。我的选择是一个哈希表,我认为这差不多是默认的。



0

除非您期望符号表很小,否则我应该避免使用链表。一千个项目的列表平均需要500次迭代才能找到其中的任何项目。

只要平衡,二叉树就可以快得多。如果您要保留内容,则序列化的表单可能会被排序,并且在重新加载时,结果树将完全不平衡,其行为与链接列表相同-因为那样基本上已经变成了什么。平衡树算法可以解决此问题,但会使整个Shebang变得更加复杂。

哈希图(只要您选择合适的哈希算法)看起来就是最好的解决方案。您没有提到您的环境,但是几乎所有现代语言都内置了Hashmap。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.