二叉树 vs. 链表 vs. 哈希表
Posted
技术标签:
【中文标题】二叉树 vs. 链表 vs. 哈希表【英文标题】:Binary Trees vs. Linked Lists vs. Hash Tables 【发布时间】:2010-09-27 03:13:21 【问题描述】:我正在为我正在进行的项目构建符号表。我想知道人们对存储和创建符号表的各种方法的优缺点有何看法。
我进行了相当多的搜索,最常推荐的是二叉树或链表或哈希表。以上所有的优点和缺点是什么? (在 C++ 中工作)
【问题讨论】:
相关:***.com/questions/4128546/… 【参考方案1】:当然,这取决于几件事。我会说链接列表是正确的,因为它几乎没有合适的属性可以用作符号表。如果您已经拥有二叉树并且不必花时间编写和调试它,二叉树可能会起作用。我的选择是哈希表,我认为这或多或少是用于此目的的默认值。
【讨论】:
【参考方案2】:This question 在 C# 中遍历不同的容器,但在您使用的任何语言中它们都是相似的。
【讨论】:
【参考方案3】:这些数据结构之间的标准权衡适用。
二叉树 实现中等复杂度(假设您无法从库中获取它们) 插入是 O(logN) 查找是 O(logN) 链接列表(未排序) 实现复杂度低 插入是 O(1) 查找是 O(N) 哈希表 实施复杂度高 插入平均为 O(1) 平均查找时间为 O(1)【讨论】:
对于未排序的链表,插入是 O(1),而不是 O(N),当双向链接时,与 O(1) 删除一起,通常是使用它们的动机,而不是它们的实现复杂性。另一个动机是他们可以无限制地成长,无需复制。并不是说在这种情况下我会建议一个。 我还认为哈希表与正确平衡的二叉树一样容易实现。但这是非常主观的。 是的,实现复杂性是主观的。但我认为最小链表比最小哈希表更简单。然后添加自动平衡与碰撞并在满时调整大小不会交换顺序。 二叉树的一个特点是它们允许(键)排序迭代。 删除操作呢?【参考方案4】:您的用例大概是“插入一次数据(例如,应用程序启动),然后执行大量读取,但如果有额外插入,则很少”。
因此,您需要使用一种能够快速查找所需信息的算法。
因此,我认为 HashTable 是最适合使用的算法,因为它只是生成密钥对象的哈希并使用它来访问目标数据 - 它是 O(1)。其他是 O(N) (大小为 N 的链接列表 - 您必须一次遍历一个列表,平均 N/2 次)和 O(log N) (二叉树 - 您将搜索空间减半每次迭代 - 仅当树是平衡的,所以这取决于您的实现,不平衡的树的性能可能会明显更差)。
只需确保 HashTable 中有足够的空间(存储桶)用于存储您的数据(例如,Soraz 对这篇文章的评论)。大多数框架实现(Java、.NET 等)都具有您无需担心实现的质量。
你在大学里修过数据结构和算法课程吗?
【讨论】:
高中还没毕业……所以没有。都是自学的:) O(1) 用于哈希表查找仅适用于桶数占总集合的很大一部分的情况。 IE。如果您在 512 个桶中存储 100 万个条目,那么您仍将进行 2048 次直接比较 pr 查找,这超过 100 万的 log(n)(或 13 次直接比较 pr 查找) 哈希表的高质量实现,使用高质量的哈希算法将给出 O(1)。二叉树的糟糕实现也可能比 O(log N) 更糟糕。因此,对于所提出的问题级别,说哈希表是 O(1) 可能已经足够好了。 符号表还有其他属性,这使得哈希表通常不是最合适的。 -1 @Stephan:详细说明。我声称哈希表是迄今为止用于符号表的最常见的数据结构。【参考方案5】:除非你希望你的符号表很小,否则我应该避开链表。包含 1000 个项目的列表平均需要 500 次迭代才能找到其中的任何项目。
二叉树可以更快,只要它是平衡的。如果您要保留内容,则序列化表单可能会被排序,并且当它重新加载时,结果树将完全不平衡,并且它的行为与链表相同 - 因为那是基本上它变成了什么。平衡树算法解决了这个问题,但使整个 shebang 更加复杂。
哈希图(只要您选择合适的哈希算法)看起来是最好的解决方案。您没有提到您的环境,但几乎所有现代语言都内置了 Hashmap。
【讨论】:
【参考方案6】:有几点需要注意。
如果二叉树是平衡的,则二叉树只有 O(log n) 的查找和插入复杂度。如果您的符号以非常随机的方式插入,这应该不是问题。如果它们按顺序插入,您将构建一个链表。 (对于您的特定应用程序,它们不应该按任何顺序排列,所以应该没问题。)如果符号可能过于有序,Red-Black Tree 是更好的选择。
哈希表的平均插入和查找复杂度为 O(1),但这里也有一个警告。如果你的哈希函数很糟糕(我的意思是真的很糟糕),你最终也可以在这里建立一个链表。但是,任何合理的字符串散列函数都应该这样做,所以这个警告实际上只是为了确保您知道它可能会发生。您应该能够测试您的哈希函数在您的预期输入范围内没有很多冲突,您会没事的。另一个小缺点是如果您使用的是固定大小的哈希表。大多数哈希表实现在达到一定大小时会增长(更精确的负载因子,请参阅here 了解详细信息)。这是为了避免在将一百万个符号插入十个桶时遇到的问题。这只会导致十个平均大小为 100,000 的链表。
如果我有一个非常短的符号表,我只会使用链表。它最容易实现,但链表的最佳情况性能是其他两个选项的最差情况性能。
【讨论】:
至于1:这是一个好点。当我过去实现符号表时,我通常发现我的条目几乎是以随机(字母顺序)顺序出现的。正因为如此,确实没有足够的回报来平衡这棵树。【参考方案7】:大家似乎忘记了,对于小的 Ns,IE 表中很少的符号,链表可以比哈希表快得多,尽管理论上它的渐近复杂度确实更高。
Pike 的 C 语言编程笔记中有一句名言:“规则 3。当 n 小时,花哨的算法很慢,而 n 通常很小。花哨的算法有很大的常数。直到你知道 n 经常要大,不要花哨。” http://www.lysator.liu.se/c/pikestyle.html
我无法从您的帖子中判断您是否会处理小 N,但请始终记住,大 N 的最佳算法不一定适用于小 N。
【讨论】:
这取决于实现。如果您碰巧知道计算哈希值的算法,您可以大致了解它与 n/2 身份比较(链表的平均值)或 log(n) 身份比较(二叉树的平均值)相比有多昂贵. 你没有提到你正在使用哪种语言,但如果它对字典/哈希表/whatever-that-lang-called-it 有很好的内置支持,例如 Python,它就是可能最容易学会停止担心并爱上内置。 正如 Hank 所写的那样,在不知道的情况下无法猜测 big 的限制:你的输入数据集、你的哈希算法、你的编程语言(字符串是否被实习)等等。通常你可以得到它错误地了解以上所有内容。使用最容易编码的部分,如果速度较慢,请稍后修复。 另外,平均对于二叉树应该是 (log n) / 2 另外,“调试奇怪错误的时间”对于花哨的算法要高得多。保持简单,直到简单被证明是站不住脚的。【参考方案8】:我喜欢比尔的回答,但它并没有真正综合起来。
从三个选项中:
链接列表从 (O(n)) 中查找项目相对较慢。因此,如果您的表中有 很多 项,或者您要进行大量查找,那么它们不是最佳选择。但是,它们很容易构建,也很容易编写。如果表很小,并且/或者您在构建后只对其进行一次小扫描,那么这可能是您的选择。
哈希表可以非常快。然而,为了让它工作,你必须为你的输入选择一个好的散列,并且你必须选择一个足够大的表来容纳所有东西,而不会出现很多散列冲突。这意味着您必须了解输入的大小和数量。如果你把它搞砸了,你最终会得到一组非常昂贵和复杂的链表。我会说,除非您提前知道表的大小,否则不要使用哈希表。这与您的“已接受”答案不一致。对不起。
留下树木。不过,您可以在这里选择:平衡或不平衡。通过研究我们这里的 C 和 Fortran 代码上的这个问题,我发现符号表输入往往是足够随机的,如果不平衡树,你只会损失大约一两个树级别。鉴于平衡树插入元素的速度较慢且难以实现,因此我不会打扰它们。但是,如果您已经可以访问很好的调试组件库(例如:C++ 的 STL),那么您不妨继续使用平衡树。
【讨论】:
虽然我同意你关于 HashTables 的观点,但我的回答是针对一个非常具体的用例 - 读取一次,添加少量(如果有)和大量读取 - 因此假设 HashTable 属于正确的大小(自动增长或设置为输入的 1.2 倍大小)这是最好的选择。 提前知道输入大小的情况是一种相当不寻常的特殊情况。在那种特殊情况下,当然,使用哈希表。但是 Ben 没有给出任何迹象表明 他的 案子符合这种罕见的条件。【参考方案9】:听起来以下可能都是真的:
您的键是字符串。 插入只完成一次。 经常进行查找。 键值对的数量相对较少(例如,少于 K 左右)。如果是这样,您可能会考虑在任何其他结构上使用排序列表。这在插入过程中会比其他的表现更差,因为排序列表在插入时是 O(N),而对于链表或哈希表是 O(1),而对于排序列表是 O(log2N)平衡二叉树。但是在排序列表中查找可能比任何其他结构都快(我将很快解释这一点),因此您可能会排在首位。此外,如果您一次执行所有插入(或者在所有插入完成之前不需要查找),那么您可以将插入简化为 O(1) 并在最后进行更快的排序。更重要的是,排序列表使用的内存比任何其他结构都少,但唯一可能重要的是如果您有许多小列表。如果您有一个或几个大列表,那么哈希表的性能可能会优于排序列表。
为什么使用排序列表查找会更快?好吧,很明显它比链表快,后者的查找时间为 O(N)。对于二叉树,如果树保持完美平衡,查找仅保持 O(log2 N)。保持树平衡(例如红黑)增加了复杂性和插入时间。此外,对于链表和二叉树,每个元素都是一个单独分配的1节点,这意味着您必须取消引用指针并可能跳转到潜在的广泛变化内存地址,增加缓存未命中的机会。
关于哈希表,您可能应该在 *** 上阅读 a couple 或 other questions,但这里的主要兴趣点是:
在最坏的情况下,哈希表可以退化为 O(N)。 散列的成本不为零,在某些实现中它可能很重要,尤其是在字符串的情况下。 与链表和二叉树一样,每个条目都是一个节点,不仅存储键和值,在某些实现中也是单独分配的,因此您使用更多内存并增加缓存的机会错过。当然,如果您真的关心这些数据结构的性能,您应该测试它们。对于大多数常用语言,您应该可以轻松找到其中任何一个的良好实现。将您的一些真实数据放入这些数据结构中的每一个并查看哪个表现最佳应该不会太难。
-
实现可以预先分配节点数组,这将有助于解决缓存未命中问题。我在链表或二叉树的任何实际实现中都没有看到这一点(当然,我并不是每个都见过),尽管你当然可以自己动手。不过,缓存未命中的可能性仍然略高,因为 node 对象必然大于键/值对。
【讨论】:
对于哈希表(在这种情况下),可以达到 O(1),因为您事先知道所有将在其中进行哈希处理的数据。所以,我猜排序数组的唯一优势就是空间复杂度。【参考方案10】:其他 cmets 专注于添加/检索元素,但如果不考虑迭代整个集合需要什么,这个讨论是不完整的。这里简短的回答是,哈希表需要更少的内存来迭代,但树需要更少的时间。
对于哈希表,迭代(键,值)对的内存开销不取决于表的容量或表中存储的元素数量;事实上,迭代应该只需要一个或两个索引变量。
对于树,所需的内存量始终取决于树的大小。您可以在迭代时维护未访问节点的队列,也可以向树添加额外的指针以便于迭代(为了迭代的目的,使树像链表一样),但无论哪种方式,您都必须为迭代分配额外的内存.
但在时机方面情况正好相反。对于哈希表,迭代所需的时间取决于表的容量,而不是存储元素的数量。因此,以 10% 的容量加载的表将比具有相同元素的链表花费大约 10 倍的时间来迭代!
【讨论】:
以上是关于二叉树 vs. 链表 vs. 哈希表的主要内容,如果未能解决你的问题,请参考以下文章