什么是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);
}
}
}