從Trie樹到雙陣列Trie樹
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了事,無論是TreeMap的對數複雜度,還是HashMap的鉅額空間複雜度與雜湊函式的效能消耗,都會降低整體效能。
雙陣列Trie樹能高速O(n)完成單串匹配,並且記憶體消耗可控,然而軟肋在於多模式匹配,如果要匹配多個模式串,必須先實現字首查詢,然後頻繁擷取文字字尾才可多匹配,這樣一份文字要回退掃描多遍,效能極低。
如果能用雙陣列Trie樹表達AC自動機,就能集合兩者的優點,得到一種近乎完美的資料結構。在我的Java實現中,我稱其為AhoCorasickDoubleArrayTrie,支援泛型和持久化,自己非常喜愛。
作者:Jadepeng 出處:jqpeng的技術記事本--http://www.cnblogs.com/xiaoqi 您的支援是對博主最大的鼓勵,感謝您的認真閱讀。 本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。