Trie樹
原理
又稱單詞查找樹,Trie樹,是一種樹形結構,是一種哈希樹的變種。它的優點是:利用字符串的公共前綴來減少查詢時間,最大限度地減少無謂的字符串比較,能在常數時間O(len)內實現插入和查詢操作,是一種以空間換取時間的數據結構,廣泛用於詞頻統計和輸入統計領域。
來看看Trie樹長什么樣,我們從百度找一張圖片:
字典樹在查找時,先看第一個字是否在字典樹里,如果在繼續往下,如果不在,則字典里不存在,因此,對於一個長度為len的字符串,可以在O(len)時間內完成查詢。
實現trie樹
怎么實現trie樹呢,trie樹的關鍵是一個節點要在O(1)時間跳轉到下一級節點,因此鏈表方式不可取,最好用數組來存儲下一級節點。問題就來了,如果是純英文字母,長度26的數組就可以搞定,N個節點的數,就需要N個長度為26的數組。但是,如果包含中文等字符呢,就需要N個65535的數組,特別占用存儲空間。當然,可以考慮使用map來存儲下級節點。
定義一個Node,包含節點的Character word,以及下級節點nexts和節點可能附件的值values:
public static class Node<T> {
Character word;
List<T> values;
Map<Character, Node> nexts = new HashMap<>(24);
public Node() {
}
public Node(Character word) {
this.word = word;
}
public Character getWord() {
return word;
}
public void setWord(Character word) {
this.word = word;
}
public void addValue(T value){
if(values == null){
values = new ArrayList<>();
}
values.add(value);
}
public List<T> getValues() {
return values;
}
public Map<Character, Node> getNexts() {
return nexts;
}
/**
* @param node
*/
public void addNext(Node node) {
this.nexts.put(node.getWord(), node);
}
public Node getNext(Character word) {
return this.nexts.get(word);
}
}
來看如何構建字典樹,首先定義一棵樹,包含根節點即可
public static class Trie<T> {
Node<T> rootNode;
public Trie() {
this.rootNode = new Node<T>();
}
public Node<T> getRootNode() {
return rootNode;
}
}
構建樹,拆分成單字,然后逐級構建樹。
public static class TrieBuilder {
public static Trie<String> buildTrie(String... values){
Trie<String> trie = new Trie<String>();
for(String sentence : values){
// 根節點
Node<String> currentNode = trie.getRootNode();
for (int i = 0; i < sentence.length(); i++) {
Character character = sentence.charAt(i);
// 尋找首個節點
Node<String> node = currentNode.getNext(character);
if(node == null){
// 不存在,創建節點
node = new Node<String>(character);
currentNode.addNext(node);
}
currentNode = node;
}
// 添加數據
currentNode.addValue(sentence);
}
return trie;
}
Trie樹應用
比如判斷一個詞是否在字典樹里,非常簡單,逐級匹配,末了判斷最后的節點是否包含數據:
public boolean isContains(String word) {
if (word == null || word.length() == 0) {
return false;
}
Node<T> currentState = rootNode;
for (int i = 0; i < word.length(); i++) {
currentState = currentState.getNext(word.charAt(i));
if (currentState == null) {
return false;
}
}
return currentState.getValues()!=null;
}
測試代碼:
public static void main(String[] args) {
Trie trie = TrieBuilder.buildTrie("劉德華","劉三姐","劉德剛","江姐");
System.out.println(trie.isContains("劉德華"));
System.out.println(trie.isContains("劉德"));
System.out.println(trie.isContains("劉大大"));
}
結果:
true
false
false
雙數組Trie樹
在Trie數實現過程中,我們發現了每個節點均需要 一個數組來存儲next節點,非常占用存儲空間,空間復雜度大,雙數組Trie樹正是解決這個問題的。雙數組Trie樹(DoubleArrayTrie)是一種空間復雜度低的Trie樹,應用於字符區間大的語言(如中文、日文等)分詞領域。
原理
雙數組的原理是,將原來需要多個數組才能表示的Trie樹,使用兩個數據就可以存儲下來,可以極大的減小空間復雜度。具體來說:
使用兩個數組base和check來維護Trie樹,base負責記錄狀態,check負責檢查各個字符串是否是從同一個狀態轉移而來,當check[i]為負值時,表示此狀態為字符串的結束。
上面的有點抽象,舉個例子,假定兩個單詞ta,tb,base和check的值會滿足下面的條件:
base[t] + a.code = base[ta]
base[t] + b.code = base[tb]
check[ta] = check[tb]
在每個節點插入的過程中會修改這兩個數組,具體說來:
1、初始化root節點base[0] = 1; check[0] = 0;
2、對於每一群兄弟節點,尋找一個begin值使得check[begin + a1…an] == 0,也就是找到了n個空閑空間,a1…an是siblings中的n個節點對應的code。
3、然后將這群兄弟節點的check設為check[begin + a1…an] = begin
4、接着對每個兄弟節點,如果它沒有孩子,令其base為負值;否則為該節點的子節點的插入位置(也就是begin值),同時插入子節點(迭代跳轉到步驟2)。
碼表:
膠 名 動 知 下 成 舉 一 能 天 萬
33014 21517 21160 30693 19979 25104 20030 19968 33021 22825 19975
DoubleArrayTrie{
char = × 一 萬 × 舉 × 動 × 下 名 × 知 × × 能 一 天 成 膠
i = 0 19970 19977 20032 20033 21162 21164 21519 21520 21522 30695 30699 33023 33024 33028 40001 44345 45137 66038
base = 1 2 6 -1 20032 -2 21162 -3 5 21519 -4 30695 -5 -6 33023 3 1540 4 33024
check= 0 1 1 20032 2 21162 3 21519 1540 4 30695 5 33023 33024 6 20032 21519 20032 33023
size=66039, allocSize=2097152, key=[一舉, 一舉一動, 一舉成名, 一舉成名天下知, 萬能, 萬能膠], keySize=6, progress=6, nextCheckPos=33024, error_=0}
首層:一[19968],萬[ 19975]
base[一] = base[0]+19968-19968 = 1
base[萬] = base[0]+19975-19968 =
實現
參考 雙數組Trie樹(DoubleArrayTrie)Java實現
開源項目:https://github.com/komiya-atsushi/darts-java
雙數組Trie+AC自動機
參見:http://www.hankcs.com/program/algorithm/aho-corasick-double-array-trie.html
結合了AC自動機+雙數組Trie樹:
AC自動機能高速完成多模式匹配,然而具體實現聰明與否決定最終性能高低。大部分實現都是一個Map<Character, State>了事,無論是TreeMap的對數復雜度,還是HashMap的巨額空間復雜度與哈希函數的性能消耗,都會降低整體性能。
雙數組Trie樹能高速O(n)完成單串匹配,並且內存消耗可控,然而軟肋在於多模式匹配,如果要匹配多個模式串,必須先實現前綴查詢,然后頻繁截取文本后綴才可多匹配,這樣一份文本要回退掃描多遍,性能極低。
如果能用雙數組Trie樹表達AC自動機,就能集合兩者的優點,得到一種近乎完美的數據結構。在我的Java實現中,我稱其為AhoCorasickDoubleArrayTrie,支持泛型和持久化,自己非常喜愛。
作者:Jadepeng
出處:jqpeng的技術記事本--http://www.cnblogs.com/xiaoqi
您的支持是對博主最大的鼓勵,感謝您的認真閱讀。
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。