字典树:又称单词查找树/前缀树/Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
自动补全
拼写检查
文字处理软件中的拼写检查 IP路由(最长前缀匹配) 使用Trie树的最长前缀匹配算法,Internet 协议(IP)路由中利用转发表选择路径 T9(九宫格)打字预测 九宫格输入 单词游戏 Trie 树可通过剪枝搜索空间来高效解决 Boggle 单词游戏还有其他的数据结构,如平衡树和哈希表,使我们能够在字符串数据集中搜索单词。为什么我们还需要 Trie 树呢?尽管哈希表可以在 O ( 1 ) O(1) O(1)时间内寻找键值,却无法高效的完成以下操作:
找到具有同一前缀的全部键值。按词典序枚举字符串的数据集。Trie 树优于哈希表的另一个理由是,随着哈希表大小增加,会出现大量的冲突,时间复杂度可能增加到 O ( n ) O(n) O(n),其中 n n n是插入的键的数量。与哈希表相比,Trie 树在存储多个具有相同前缀的键时可以使用较少的空间。此时 Trie 树只需要 O ( m ) O(m) O(m) 的时间复杂度,其中 m m m为键长。而在平衡树中查找键值需要 O ( m l o g n ) O(mlogn) O(mlogn) 时间复杂度。
最大的问题:空间!
Trie 树是一个有根的树,其结点具有以下字段:
指向子结点的链接next,其中每个链接对应字母表数据集中的一个字母,本文通过Map来存储,key是当前节点对应的字符,value是子节点布尔字段isWord,以指定节点是对应键的结尾还是只是键前缀 private class Node { public boolean isWord; public TreeMap<Character, Node> next; public Node(boolean isWord) { this.isWord = isWord; next = new TreeMap<>(); } public Node() { this(false); } }通过搜索 Trie 树来插入一个单词。从根节点开始搜索它对应于第一个字符的链接。有两种情况:
链接存在。沿着链接移动到树的下一个子层。算法继续搜索下一个字符。链接不存在。创建一个新的节点,并将它与父节点的链接相连,该链接与当前单词的字符相匹配。重复以上步骤,直到到达单词的最后一个字符,然后将当前节点标记为结束节点,插入完成
public void add(String word) { Node cur = root; for (int i = 0; i < word.length(); i++) { char c = word.charAt(i); if (cur.next.get(c) == null) { cur.next.put(c, new Node()); } cur = cur.next.get(c); } // 判断是否是新单词 if ( !cur.isWord) { cur.isWord = true; size++; } }复杂度分析
时间复杂度: O ( m ) O(m) O(m),其中 m m m 为单词长度。在每次迭代中,要么检查要么创建一个节点,直到到达词尾。只需要 m m m 次操作。
空间复杂度: O ( m ) O(m) O(m)。最坏的情况下,新插入的单词和 Trie 树中已有的单词没有公共前缀。此时需要添加 m m m 个结点,使用 O ( m ) O(m) O(m) 空间
每个单词在 Trie 中表示为从根节点到内部节点或叶的路径。我们从根节点开始,检查当前节点中与该单词字符对应的链接。有两种情况:
存在链接。移动到该链接后面路径中的下一个节点,并继续搜索下一个键字符。不存在链接。若已遍历完单词,且当前结点标记为 isWord,则返回 true。否则有两种情况,均返回 false : 单词未遍历结束, 但无法跟随Trie树的字符路径,即Trie中不包含此单词单词已遍历完毕,但是该单词只是Trie中另一个单词的前缀 public boolean contains(String word) { Node cur = root; for (int i = 0; i < word.length(); i++) { char c = word.charAt(i); if (cur.next.get(c) == null) { return false; } cur = cur.next.get(c); } return cur.isWord; }复杂度分析
时间复杂度 : O ( m ) O(m) O(m)。算法的每一步均搜索下一个字符。最坏的情况下需要 m m m 次操作。空间复杂度 : O ( 1 ) O(1) O(1)。与上面提到的“查询单词”算法唯一的区别是,到达待查找前缀的末尾时,总是返回 true。不需要考虑当前 Trie 节点是否用 “isWord” 标记,因为我们搜索的是前缀,而不是整个单词。
public boolean isPrefix(String prefix) { Node cur = root; for (int i = 0; i < prefix.length(); i++) { char c = prefix.charAt(i); if (cur.next.get(c) == null) { return false; } cur = cur.next.get(c); } return true; }复杂度分析
时间复杂度 : O ( m ) O(m) O(m)。空间复杂度 : O ( 1 ) O(1) O(1)。Github仓库
LeetCode 208. 实现Trie(前缀树)
LeetCode 211. 添加与搜索单词 - 数据结构设计