Trie树(发音同“try”),又称为前缀树、字典树,从名字来看,它首先是一种树形的结构,“前缀”、“字典”等字样表明其存储的是数据的前缀,像字典一样,可以实现快速的查找。
实际上,Trie树是一种专门处理字符串匹配的数据结构,用来在一批字符串中,快速查找某个指定的字符串或字符串前缀。
应用场景
- 搜索建议
-
IP路由,最长前缀匹配
-
输入法-预测
Trie树的结构
先来看一个例子,比如说我们有一组字符串,分别是 ha、head、hacker、book、boss,如果要查询某个指定的字符串是否在这一组中,那么要对每个字符串进行匹配,如果这个查询操作有很多次,这种查询势必是比较耗时的。
观察一下,这一组字符串都有公共的前缀,那能不能利用这个特性,降低查询时间复杂度呢?Trie树就是为这种问题而生的,我们看一下把这一组字符串构造成trie树是什么样子的。
如上图所示,其中根节点无意义,不是字符串的组成部分,其他节点是组成字符串的字符,红色节点代表有字符串是以此节点结尾的,也就是说从根节点开始到红色节点的路径可以形成一个有效的字符串。
下图展示了一组字符串一个一个添加到trie树的过程。
在trie树中查找一个字符串是否存在,需要从trie树的根节点开始,依次匹配字符串中的每个字符,直到匹配到字符串中的最后一个字符,且trie节点是“结尾”节点。比如,在上面构建的trie树中查找字符串“boss”,查找路径如下图所示。
实现Trie树
假设字符串全部由小写字母组成,字符集则最多由26种字符组成,那么每个节点最多会有26个指针指向下一个节点,我们可以用数组存储子节点。
同时,每个节点需要一个布尔字段,用来表示这个节点是前缀还是结尾。Trie树节点的结构定义如下:
private static class TrieNode {
char c;
boolean isEnd;
TrieNode[] children;
}
我们现在来实现一个典型的Trie树,它支持三种操作:添加字符串;查找字符串;查找前缀。实现代码如下。
class Trie {
private TrieNode root;
/** Initialize your data structure here. */
public Trie() {
root = new TrieNode('/', false);
}
/** Inserts a word into the trie. */
public void insert(String word) {
if (word == null || word.length() == 0) {
return;
}
TrieNode p = root;
for (int i = 0; i < word.length(); i++) {
int idx = word.charAt(i) - 'a';
if (p.children[idx] == null) {
p.children[idx] = new TrieNode(word.charAt(i), false);
}
p = p.children[idx];
}
p.isEnd = true;
}
/** Returns if the word is in the trie. */
public boolean search(String word) {
TrieNode node = searchNode(word);
return node != null && node.isEnd;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
public boolean startsWith(String prefix) {
return searchNode(prefix) != null;
}
private TrieNode searchNode(String word) {
if (word == null || word.length() == 0) {
return null;
}
TrieNode p = root;
for (int i = 0; i < word.length(); i++) {
int idx = word.charAt(i) - 'a';
if (p.children[idx] == null) {
return null;
} else {
p = p.children[idx];
}
}
return p;
}
private static class TrieNode {
char c;
boolean isEnd;
TrieNode[] children;
public TrieNode(char c, boolean isEnd) {
this.c = c;
this.isEnd = isEnd;
children = new TrieNode[26];
}
}
}
其中,
- 添加字符串
对应的方法为 public void insert(String word)
,时间复杂度为O(n),n为字符串的长度,我们需要检查每一个字符是否存在,不存在就创建一个节点;空间复杂度为O(n),最坏情况下,每个字符都需要创建一个新的节点。
- 查找字符串前缀是否存在
对应方法为 public boolean startsWith(String prefix)
,时间复杂度为O(n),依次查询每个字符,空间复杂度为O(1)。
- 查找字符串是否存在
对应方法为 public boolean search(String word)
,时间复杂度为O(n),空间复杂度为O(1)。查询过程和查找前缀类似,只是在遍历到最后一个字符的时候,要判断该节点是否为结尾节点,是结尾节点才符合查询要求。
和哈希表的对比
哈希表的查询时间复杂度是O(1),Trie树的查询时间复杂度是O(m),m为字符串的长度,查询效率要低于哈希表,但是当数据量比较大的时候,哈希表冲突增加,时间复杂度可能退化到O(n),n为字符串的个数。
Trie树在以下特定的情况下,会有优势:
- 字符串包含的字符集不能太大,字符串数量比较大;
- 字符串前缀重合度比较高,这样可以节省存储空间;
- 查找前缀匹配的字符串,而不是精确查找
扫二维码关注我,一起讨论算法。