1. 程式人生 > 實用技巧 >Trie 樹 (字首樹, 字典樹)

Trie 樹 (字首樹, 字典樹)

參考

字典樹(字首樹)Trie樹(字典樹,字首樹,鍵樹)分析詳解Trie Tree 的實現 (適合初學者)https://leetcode-cn.com/problems/implement-trie-prefix-tree/solution/shi-xian-trie-qian-zhui-shu-by-leetcode/資料結構之Hash樹

概述

字典樹(TrieTree),又稱單詞查詢樹或鍵樹,是一種樹形結構,是一種雜湊樹的變種。Trie的核心思想是空間換時間,利用字串的公共字首來降低查詢時間的開銷以達到提高效率的目的。它有3個基本性質:
根節點不包含字元,除根節點外每一個節點都只包含一個字元。
從根節點到某一節點,路徑上經過的字元連線起來,為該節點對應的字串。
每個節點的所有子節點包含的字元都不相同。

應用

Trie樹典型應用是用於快速檢索(最長字首匹配),自動補全,拼寫檢查,統計,排序和儲存大量的字串,所以經常被搜尋引擎系統用於文字詞頻統計,搜尋提示等場景。它的優點是最大限度地減少無謂的字串比較,查詢效率比較高。

Trie樹與二叉搜尋樹

資料規模為n時,二叉搜尋樹插入、查詢、刪除操作的時間複雜度通常只有O(logn),最壞情況下整棵樹所有的節點都只有一個子節點,退變成一個線性表,此時插入、查詢、刪除操作的時間複雜度是O(n)。

   通常情況下,Trie樹的高度n要遠大於搜尋字串的長度m,故查詢操作的時間複雜度通常為O(m),最壞情況下(當字串非常長)的時間複雜度才為O(n)。很容易看出,Trie樹最壞情況下的查詢也快過二叉搜尋樹。

Trie樹與Hash表

既然有了其他的資料結構,如平衡樹和雜湊表,使我們能夠在字串資料集中搜索單詞。為什麼我們還需要 Trie 樹呢?儘管雜湊表可以在 O(1) 時間內尋找鍵值,卻無法高效的完成以下操作:
  • 找到具有同一字首的全部鍵值。
  • 按詞典序列舉字串的資料集。
Trie 樹優於雜湊表的另一個理由是,隨著雜湊表大小增加,會出現大量的衝突,時間複雜度可能增加到 O(n),其中 n 是插入的鍵的數量。與雜湊表相比,Trie 樹在儲存多個具有相同字首的鍵時可以使用較少的空間。此時 Trie 樹只需要O(m) 的時間複雜度,其中 m 為鍵長。而在平衡樹中查詢鍵值需要 O(mlogn) 時間複雜度。

定義類 Trie

正常的樹節點定義是怎麼樣的

struct TreeNode {
    VALUETYPE value;    //結點值
    TreeNode* children[NUM];    //指向孩子結點
};

下面是Trie的結點定義,體會二者的不同

class Trie {

    boolean isEnd = false;      // 標記是否為最終結點
    Trie[] next;        // 所有孩子結點

    /** Initialize your data structure here. */
    public Trie() {
        next = new Trie[26];    // 26個孩子結點
    }
}

插入

描述:向 Trie 中插入一個單詞 word

實現:這個操作和構建連結串列很像。首先從根結點的子結點開始與 word 第一個字元進行匹配,一直匹配到字首鏈上沒有對應的字元,這時開始不斷開闢新的結點,直到插入完 word 的最後一個字元,同時還要將最後一個結點isEnd = true;,表示它是一個單詞的末尾。

public void insert(String word) {
    // 有則共享且繼續向下遍歷,否則新建結點
    Trie node = this;       // node指標用來遍歷
    for(char ch : word.toCharArray()){
        int index = ch - 'a';
        if(node.next[index] == null){   // 新建結點
            node.next[index] = new Trie();
        }
        node = node.next[index];    // 指標後移
    }
    node.isEnd = true;      // 標記為最終結點
}

查詢

描述:查詢 Trie 中是否存在單詞 word

實現:從根結點的子結點開始,一直向下匹配即可,如果出現結點值為空就返回 false,如果匹配到了最後一個字元,那我們只需判斷 node->isEnd即可。

public boolean find(String word) {
    // 有則共享且繼續向下遍歷,否則新建結點
    Trie node = this;       // node指標用來遍歷
    for(char ch : word.toCharArray()){
        int index = ch - 'a';
        if(node.next[index] == null){   // 新建結點
            return false;
        }
        node = node.next[index];    // 指標後移
    }
    return node.isEnd;
}

字首匹配

描述:判斷 Trie 中是或有以 prefix 為字首的單詞

實現:和 search 操作類似,只是不需要判斷最後一個字元結點的isEnd,因為既然能匹配到最後一個字元,那後面一定有單詞是以它為字首的

public boolean startsWith(String prefix) {
    // 和search方法其實差不多,但是結束判斷後直接返回true, 因為是判斷字首而已
    Trie node = this;
    for(char ch : prefix.toCharArray()){
        int index = ch - 'a';
        if(node.next[index] == null){
            return false;
        }
        node = node.next[index];    // 指標後移
    }
    return true;
}
總結

通過以上介紹和程式碼實現我們可以總結出 Trie 的幾點性質:

  • Trie 的形狀和單詞的插入或刪除順序無關,也就是說對於任意給定的一組單詞,Trie 的形狀都是唯一的。
  • 查詢或插入一個長度為 L 的單詞,訪問 next 陣列的次數最多為 L+1,和 Trie 中包含多少個單詞無關。
  • Trie 的每個結點中都保留著一個字母表,這是很耗費空間的。如果 Trie 的高度為 n,字母表的大小為 m,最壞的情況是 Trie 中還不存在字首相同的單詞,那空間複雜度就為 O(m^n)。
最後,關於 Trie 的應用場景,希望你能記住 8 個字:一次建樹,多次查詢