Trie,又称字典树、单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
上图是一棵Trie树,表示了关键字集合{“a”, “to”, “tea”, “ted”, “ten”, “i”, “in”, “inn”} 。从上图可以归纳出Trie树的基本性质:
- 根节点不包含字符,除根节点外的每一个子节点都包含一个字符。
- 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。
- 每个节点的所有子节点包含的字符互不相同。
- 从第一字符开始有连续重复的字符只占用一个节点,比如上面的to,和ten,中重复的单词t只占用了一个节点。
字典树的实现有递归与非递归的两种方式:
非递归:
非递归的实现也有两种方式,一种是利用自己创建的节点来实现,具体实现如下:
public class Trie {
public static void main(String[] args) {
Trie trie = new Trie();
trie.add("apple");
System.out.println(trie.contains("apple"));
System.out.println(trie.isPrefix("app"));
}
private class TreeNode{
public boolean isWord;
public TreeMap<Character,TreeNode> next;
public TreeNode(){
this(false);
next = new TreeMap<>();
}
public TreeNode(boolean isWord){
this.isWord = isWord;
}
}
private TreeNode root;
private int size;
public Trie(){
this.root = new TreeNode();
this.size = 0;
}
public int getSize(){
return size;
}
public void add(String str){
TreeNode cur = root;
for(int i=0;i<str.length();i++){
char c = str.charAt(i);
if(cur.next.get(c) == null){
//新建节点
cur.next.put(c, new TreeNode());
}
//否则,就直接走到该节点位置即可
cur = cur.next.get(c);
}
if(!cur.isWord){
//确定cur是新的单词
cur.isWord = true;
size++;
}
}
public boolean contains(String str){
TreeNode cur = root;
for(int i=0;i<str.length();i++){
char c = str.charAt(i);
if(cur.next.get(c) == null) return false;
cur = cur.next.get(c);
}
return cur.isWord;
}
public boolean isPrefix(String prefix){
TreeNode 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;
}
}
这种实现方式是利用自己创建的节点实现的。每一个Trie对象内部都存在一个TreeNode对象与size对象。其中TreeNode对象内部存在着一个TreeMap,TreeMap内存储着每一个字符与下一个节点的对应关系。
当然,也有另一种实现方式:
class Trie {
private Trie[] children;
private boolean isWord;
public Trie() {
children = new Trie[26];
isWord = false;
}
public void insert(String word) {
Trie node = this;
for(char c : word.toCharArray()){
int idx = c - 'a';
if(node.children[idx] == null){
node.children[idx] = new Trie();
}
node = node.children[idx];
}
node.isWord = true;
}
public boolean search(String word) {
Trie node = searchPrefix(word);
return node != null && node.isWord;
}
public boolean startsWith(String prefix) {
return searchPrefix(prefix) != null;
}
public Trie searchPrefix(String word){
Trie node = this;
for(char c : word.toCharArray()){
int idx = c - 'a';
if(node.children[idx] != null){
node = node.children[idx];
}else{
return null;
}
}
return node;
}
}
/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* boolean param_2 = obj.search(word);
* boolean param_3 = obj.startsWith(prefix);
*/
这个实现方式是每一个Trie对象内部存储自己的Trie[]数组,由此实现节点的对应关系。
递归
public void recursionAdd(String word) {
Node cur = root;
add(root, word, 0);
}
/**
* 递归写法调用方法实现递归添加
*
* @param node 传入要进行添加的节点
* @param word 传入要进行添加的单词
*/
public void add(Node node, String word, int index) {
// 确定终止条件,这个终止条件在没加index这个参数时,很难确定
// 此时一个单词已经遍历完成了,如果这个结束节点没有标记为单词,就标记为单词
if (!node.isWord && index == word.length()) {
node.isWord = true;
size++;
}
if (word.length() > index) {
char addLetter = word.charAt(index);
// 判断trie的下个节点组中是否有查询的字符,如果没有,就添加
if (node.next.get(addLetter) == null) {
node.next.put(addLetter, new Node());
}
// 基于已经存在的字符进行下个字符的递归查询
add(node.next.get(addLetter), word, index + 1);
}
}