從Trie樹到雙數組Trie樹


Trie樹

原理

又稱單詞查找樹,Trie樹,是一種樹形結構,是一種哈希樹的變種。它的優點是:利用字符串的公共前綴來減少查詢時間,最大限度地減少無謂的字符串比較,能在常數時間O(len)內實現插入和查詢操作,是一種以空間換取時間的數據結構,廣泛用於詞頻統計和輸入統計領域。

來看看Trie樹長什么樣,我們從百度找一張圖片:

enter description here

字典樹在查找時,先看第一個字是否在字典樹里,如果在繼續往下,如果不在,則字典里不存在,因此,對於一個長度為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
您的支持是對博主最大的鼓勵,感謝您的認真閱讀。
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM