什么是Trie?
Trie树,也叫作字典树或前缀树,顾名思义,它是一个树行结构。它是一种专门处理字符串匹配的数据结构,用来解决在一组字符串集合中快速查找某个字符串的问题。
它的核心思想就是通过最大限度地减少无畏的字符串比较,使得查询效率高效,即用空间换时间,再利用共同前缀来提高查询效率。
例如:通讯录
Trie存储结构如图:
每个节点有26个指向下个节点的指针(为了方便,这里就弄这几个节点)。节点代码如下:
public class Node{
char c;
Node next[26];
}
注意:
以上中并没有考虑到大小写问题,如果要包含大小写,就得把26个指针改成52个指针。
再比如如果存储更复杂的数据,例如存储网址,需要考虑到@、//、:等这些符号,正因为这个原因,想要设计一个更灵活的Trie,通常不会固定每一个节点只有26个指向下一个节点的指针,除非非常的肯定Trie处理的内容只包含小写的字母。
通常,只要每个节点右若干个指向下一个节点的指针,在这个描述里,只把26个指针这样的静态的数组描写成了若干,它背后其实就是一个动态的思想。
代码如下:
public class Node{
char c;
Map<char, Node> next;
}
接下来,我们再看另一个问题,Trie当中每个节点都包含一个字母。
但是,大家想象一下,其实从根节点找到下一个节点的过程中,我就已经知道这个字母已经是谁了,例如从根节点搜索cat这个词,之所以能够来到c节点,是因为在根节点就知道了下一个要到c字母所在的节点,所以,更准确的来讲我们应该把标在边上,我们是来到这个节点之前就已经知道了这个字母是什么了,才可能通过映射来找到下一个节点。所以,在节点实现中每一个节点可以不存储这个节点的值是没有任何问题的。
修改后的节点代码:
public class Node{
Map<char, Node> next;
}
还有一个问题,在Trie中查询一个单词从根节点出发到叶子节点,到了叶子节点就到了单词的地方,在这里,把叶子节点标蓝。
上图中,如果到了叶子节点t就找到了cat这个词,如果到了g就找到了dog这个词,以此类推。不过,在英语的世界中,很多的单词可能是另外单词的前缀。
什么意思呢?例如平底锅pan这个单词,如果Trie中既要存储pan这个单词,还要存储panda这个单词。此时,对于pan这个单词最后一个字母n,它并不是一个叶子节点,不然的话,就没法存储panda这个单词了。
正因为如此,在节点中需要一个标识,这个标识表达的意思就是当前的这个节点是否是一个单词的结尾,单词的结尾只靠叶子节点是区分不出来的。
修改后的节点代码:
public class Node{
boolean isWord;
Map<char, Node> next;
}
Trie的实现
1、插入元素
插入的过程非常简单,就是把要插入的元素从头到尾挨个取出来,如果存在这个元素,那么就在next中查到这个元素的下一个节点,如果不存在,添加到next中。注意:到单词结尾时需要把isWord设置为true。
2、查询元素
查询和插入的过程类似,把待查询的元素从头到尾挨个取出来,如果不存在,直接返回false。如果存储这个元素,那么就在next中查到这个元素的下一个节点,最后返回isWord。
3、前缀查询
Trie也叫作前缀树,这是为什么呢?
就是因为,在Trie中可以非常简单的去搜索某一个单词对应的前缀。
搜索过程其实非常简单,Trie存储结构:
从根节点开始向下搜索的过程其实都是在搜索单词的前缀,例如查找单词cat,找到c,c就是cat的前缀,再找到a,ca就是cat的前缀,以此类推。
所以在Trie中搜索一个单词的过程中,一路上所经过的字符串都是目标单词的前缀,正是因为这个原因,Trie也叫作前缀树。
4、代码实现
import java.util.TreeMap;
/**
* 描述:Trie树(字典树、前缀树)
* <p>
* Create By ZhangBiao
* 2020/5/16
*/
public class TrieTree {
private Node root;
private int size;
public TrieTree() {
this.root = new Node();
this.size = 0;
}
/**
* 获取Trie树中存储的单词数量
*
* @return
*/
public int getSize() {
return size;
}
/**
* 向Trie树中添加一个新的单词word
*
* @param word
*/
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++;
}
}
/**
* 查询单词word是否在Trie树中
*
* @param word
* @return
*/
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;
}
/**
* 查询是否在Trie树中有单词以prefix为前缀
*
* @param prefix
* @return
*/
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;
}
private class Node {
public boolean isWord;
public TreeMap<Character, Node> next;
public Node(boolean isWord) {
this.isWord = isWord;
this.next = new TreeMap<>();
}
public Node() {
this(false);
}
}
}