1. 程式人生 > 其它 >從Trie樹到雙陣列Trie樹

從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 您的支援是對博主最大的鼓勵,感謝您的認真閱讀。 本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。