如何实现一个字典(Trie树 vs 哈希表以及重要问题)?

16

我遇到了几个问题和文章,说 Java 中的字典实现最好使用 tries。但是据我看来,大多数文章都没有涉及到重要的问题。因此,下面是一个真实世界的任务:

假设我需要使用 Java 实现一个词典(比如 Lingvo,但更简单)。对于我的特定任务,需要存储单词定义,并执行快速的词典查找。

请回答以下问题:

  • 那么应该使用什么数据结构呢(Trie 还是 HashTable)?
  • 如果我需要词典不区分大小写,应该如何组织它(搜索、数据结构)?
  • 如果我希望它(搜索、词典)区分大小写怎么办?

P.S.:非常感谢提供代码示例。

先前的更新:如果我们在谈论 Java 中的标准 DS 实现,那么 HashTable 是否是这个特定任务的最佳选择?为什么不是 HashMap、TreeMap 或 LinkedHashMap?

3个回答

16

我只想针对你问题中的一个点进行解答:

Trie不是通用的字典数据结构。原因在于,trie是一种专门用于(子)字符串搜索的专业化搜索树。通常,您会更关心通用搜索树,例如二叉搜索树B树

所有这些实现都依赖于字典元素的排序,并且它们所有常见操作的平均和最坏情况下的运行时间都是对数级别。

相比之下,哈希表不需要元素相对顺序。相反,它需要元素是可哈希的并且可以相等比较。常见哈希表特征的最坏情况特性比树更差,即与元素数量成线性关系。

然而,通过小心处理,哈希表操作的平均情况可以做到常数(即独立于容器大小)。此外,可以证明较慢的操作非常少见。

在实践中,这意味着除非是非常专业化的用例,否则哈希表都能胜过基于树的字典。

这样做的缺点是哈希表对其元素施加了一个看似随意的顺序。如果您想按排序后的顺序获取字典中的项目,则不应使用哈希表。

(还有其他有趣的字典实现,例如跳表与搜索树竞争并且像布隆过滤器这样的概率实现。)

只有当您处理字符串值的字典时才可以使用基于trie的实现。在这种情况下,它实际上经常是一个不错的选择,特别是如果字典中的许多字符串共享共同前缀并且相当短。


如果我们谈论Java中的标准数据结构实现,那么HashTable是否是这个特定任务的最佳选择?为什么不使用HashMap、TreeMap或LinkedHashMap呢? - Denys S.
2
@den-javamaniac HashTable 是一个线程安全的 HashMap(类似于 VectorArrayList 的区别),因此当您知道多个线程不会与其交互时,使用 HashMap 更好。有趣的是,Collections.synchronizedMap(new HashMap())HashTable 更快,并且似乎提供了相同的安全性。TreeMap 要求其键是可比较的,并使用红黑树。LinkedHashMap 使用左/右/父引用(如果我没记错的话)而不是数组。这类似于 ArrayListLinkedList 之间的差异。就我个人而言,在 Java 中,我很少使用 Linked... 集合。 - KitsuneYMG
你的回答非常误导人。字典是 trie 的典型应用之一。Trie 的一个巨大优势是,当有大量数据且这些数据共享公共前缀时,它可以在内存方面发挥作用,而哈希表会出现问题。这正是字典的情况。 - Gugussee
2
@Gugussee:(已编辑!)抱歉,你是对的。我以为我已经清楚地表明我在谈论通用字典,并且tries只适用于字符串搜索。但是再看一遍,这一点并不清楚。我会更新我的答案。 - Konrad Rudolph
@RnMss 我的意思是tries有字符串(或更一般地,具有词典顺序的东西)作为键,因此不是通用的 - 尽管维基百科明确提到了一种位trie来存储浮点数,但这仍然无法很好地推广到任意类型。是的,Aho-Corasick自动机是专门针对子字符串搜索进行优化的特殊情况。 - Konrad Rudolph
显示剩余2条评论

4

编辑:请不要再点赞了,我误读了问题。原帖并不是想要一个词典来验证单词的拼写、建议、自动完成或其他类似功能(我当时认为这是他想要的)。原帖想要的是一个键值映射,其中每个单词都有一个定义。

作为一名曾经从事过词典开发的人员,我可以告诉你,你正在采用错误的方法。

这并不像选择哈希表还是trie树那么简单。

你提到了Lingvo:它远不止于一个表格。

你是否希望提供与输入单词相近的建议?那么你可能需要对用户输入的内容进行排列组合,并查看每个排列组合是否存在于词典中:如果存在,则需要计算其Levenhstein编辑距离,并首先建议具有最短LED的单词。

你是否希望自动完成/建议最可能的匹配项(就像Google所做的那样)?那么你需要一个非常高级的数据结构,例如BK树(基本上是一个LED树,如果我理解正确的话)。

你的字典中将有多少个单词?如果使用字符串和其他重量级Java对象/数据结构制作包含400,000个单词的字典,性能将会受到严重影响(再次强调:一个字典不仅仅是一个哈希表,一个字典通常涉及多个数据结构)。这将难以适应用户计算机内存。有已知的可搜索的方法来存储单词,其中每个单词可以在少于15位比特的情况下打包(没错,少于15位比特的情况下)。

除此之外,你可能还希望根据语音学进行建议,例如使用双metaphone映射。

一个“单词字典”真的远不止于一个键值表。由于用户期望的功能和涉及的数据量,它确实是一个复杂的生物。只是英语+一些专业领域的术语、医学、计算机科学等就会给你带来数十万条数据:试着把它们放入Java HashMap中,然后......炸了!


如果我有大约200,000个单词,而Levenshtein距离没有被覆盖,自动建议/完成也没有考虑进去。您会建议如何组织这个字典? - Denys S.
@den-javamiac:如果你真的想要像*contains(word)?这样简单的查询,那么一个简单的HashMap<String,String>*就可以了。然而,我也误读了问题:根据你的典型定义有多大,存储200,000个定义可能是一个问题,也可能不是。例如,如果每个“定义”都是维基百科条目的大小,那么你就有问题了。每个定义平均有多少个字符? - Gugussee
假设单词定义为50个字符。我仍然不明白为什么在需要快速字典查找和大小写敏感/不敏感的情况下,HashTable / HashMap更好。你能解释一下吗? - Denys S.

1
Java中的字典实现,使用哈希集合是最好的选择。
关于HashMap或HashTable:如果您的类以多线程方式使用,则必须使用HashTable,否则HashMap是最佳选项。
HashMap vs TreeMap: 如果需要插入顺序到集合中,则必须使用TreeMap。
HashMap vs LinkedHashMap: LinkedHashMap的实现与HashMap不同,它维护一个双向链表,该链表通过所有条目。这个链接列表定义了迭代顺序,通常是将键插入映射的顺序(插入顺序)。请注意,如果将键重新插入地图,则不会影响插入顺序。(如果在调用m.containsKey(k)之前立即返回true,则在调用m.put(k, v)时将键k重新插入地图m。)

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