[翻譯]資料結構——trie樹介紹

Xu-DongHui發表於2019-02-28

一、開篇說明

1.本文原文來自於leetcode上的演算法題Implement Trie的解決方案.
2.原文地址
3.新手獻醜了,希望大家輕拍~(微笑)


二、原文在這

1.問題描述

演算法題:
通過編寫插入、查詢、判斷開頭等方法完成一個trie樹。

例子:

Trie trie = new Trie();
trie.insert("apple");
trie.search("apple");   // returns true
trie.search("app");     // returns false
trie.startsWith("app"); // returns true
trie.insert("app");   
複製程式碼

提示:

  • 你可以假設所有的輸入都是由小寫字母a-z組成的。
  • 所有的輸入string陣列都不為空。

總結:
這篇文章是寫給中等水平的讀者的,將會介紹資料結構trie(字首樹)和其中的常見操作。

2.解決方法:

2.1應用:
trie(字首樹)是一種樹形資料結構,常常用來在字串的資料集中檢索一個關鍵詞。目前,trie資料結構已經被高效地應用在了很多領域:
(1)自動填充

谷歌實時的關鍵詞推薦
圖片1.谷歌實時的關鍵詞推薦

(2)拼寫檢查
[翻譯]資料結構——trie樹介紹
圖片2.在文書處理機中的拼寫檢查

(3)IP路由(最長的路由匹配)
[翻譯]資料結構——trie樹介紹
圖片3.最長路由匹配演算法
(4)九鍵輸入法的預測文字
[翻譯]資料結構——trie樹介紹
圖片4.九鍵輸入法在1990年代應用在手機中用來輸入文字
(5)完成文字遊戲
[翻譯]資料結構——trie樹介紹
圖片5.通過減少搜尋空間,trie能夠很好的完成boggle這個遊戲

有很多其他的資料結構如,平衡樹,hash表都能夠在一個string的資料集中查詢單詞,但是我們為什麼要使用trie呢?雖然hash表對於找到一個關鍵詞(key)只需要O (1)的時間複雜度,但是在下列操作中,它就表現得不是很高效了。

  • 找到擁有共同字首的所有關鍵詞(key)。
  • 根據字典序列舉所有字串

trie優於hash表的另外一個原因是,但hash表的資料規模擴大後,將會出現很多的hash碰撞,因此查詢時間複雜度將會提高到O (n),n 是插入的關鍵詞的個數。而相比於hash表,trie在儲存很多具有共同字首的關鍵詞時需要的空間更少。在這個例子裡trie只需要O (m)的時間複雜度(m 是關鍵詞的長度)。而在平衡樹裡查詢一個關鍵詞,則需要花費O (m logn)
2.2 trie節點結構
trie是一個有根樹,它的節點有以下幾個欄位:

  • 與子節點間最多有R個連線,每個連線對應到R個字母中的一個。這個R個字母來自於字母表。在這篇文章中,我們假設R是26,即26個小寫的拉丁字母。
  • 一個布林值isEnd,說明該布林值說明當前節點是否是一個關鍵詞的結尾,否則就只是該關鍵詞的字首。
    [翻譯]資料結構——trie樹介紹
    圖6.代表關鍵字“leet”在trie中的表達

    java編寫的trie節點
class TrieNode {

    // R links to node children
    private TrieNode[] links;

    private final int R = 26;

    private boolean isEnd;

    public TrieNode() {
        links = new TrieNode[R];
    }

    public boolean containsKey(char ch) {
        return links[ch -'a'] != null;
    }
    public TrieNode get(char ch) {
        return links[ch -'a'];
    }
    public void put(char ch, TrieNode node) {
        links[ch -'a'] = node;
    }
    public void setEnd() {
        isEnd = true;
    }
    public boolean isEnd() {
        return isEnd;
    }
}
複製程式碼

2.3 trie中最常見的操作——新增和查詢關鍵詞
(1)新增關鍵詞到trie中
我們通過遍歷trie來插入關鍵詞。我們從根節點開始,搜尋和關鍵詞第一個字母對應的連線,這裡一般有兩種情況:

  • 如果連線存在,那麼我們就順著這個連線往下移到下一子層,接著搜尋關鍵詞的下一個字母對應的連線。
  • 如果連線不存在,那麼我們就新建一個trie節點,對應著現在的關鍵詞字母,建立與父節點的連線。

我們重複這個步驟,直到處理完關鍵詞的最後一個字母,然後標記最後的節點為結束節點。演算法結束。

[翻譯]資料結構——trie樹介紹
插入關鍵字到trie中

java編寫的插入方法

class Trie {
    private TrieNode root;

    public Trie() {
        root = new TrieNode();
    }

    // Inserts a word into the trie.
    public void insert(String word) {
        TrieNode node = root;
        for (int i = 0; i < word.length(); i++) {
            char currentChar = word.charAt(i);
            if (!node.containsKey(currentChar)) {
                node.put(currentChar, new TrieNode());
            }
            node = node.get(currentChar);
        }
        node.setEnd();
    }
}
複製程式碼

複雜度分析:

  • 時間複雜度O(m),m 是關鍵詞的長度。在演算法的每一次迴圈中,我們要麼檢查節點要麼新建一個節點,直到該關鍵詞的最後一個字母。所以,這隻需要進行m次操作。
  • 空間複雜度O(m)。最糟糕的情況是,新插入的關鍵詞和已經存在trie的關鍵詞沒有共同的字首,因此我們必須插入m個新的節點,因此需要O(m)空間複雜度。

(2)在trie中搜尋關鍵詞
每一個關鍵詞在trie中都可以被一條從根節點到子節點的路徑所表示。我們將根據關鍵詞的第一個字母從根節點開始搜尋,然後檢查節點上的每一個連線對應的字母,一般有兩種情況:

  • 存在對應關鍵詞字母的連線,我們將從該連線移動到下一個節點,然後搜尋關鍵詞的下一個字母對應的連線。
  • 對應的連線不存在,如果此時已經遍歷到了關鍵詞的最後一個字母,則把當前的節點標記為結束節點,然後返回true。當然還有另外兩種情況,我們會返回false:
    • 關鍵詞的字母沒有遍歷完,但沒有辦法接著在trie中找到根據關鍵詞字母的形成的路徑,所以trie中不存在該關鍵詞。
    • 關鍵詞的字母的已經遍歷完了,但當前的節點不是結束節點,因此搜尋的關鍵詞只是trie中的某一個關鍵字的字首。

[翻譯]資料結構——trie樹介紹
圖8.在trie中搜尋某個關鍵詞
java編寫搜尋關鍵詞的方法

class Trie {
    ...

    // search a prefix or whole key in trie and
    // returns the node where search ends
    private TrieNode searchPrefix(String word) {
        TrieNode node = root;
        for (int i = 0; i < word.length(); i++) {
           char curLetter = word.charAt(i);
           if (node.containsKey(curLetter)) {
               node = node.get(curLetter);
           } else {
               return null;
           }
        }
        return node;
    }

    // Returns if the word is in the trie.
    public boolean search(String word) {
       TrieNode node = searchPrefix(word);
       return node != null && node.isEnd();
    }
}
複製程式碼

複雜度分析:

  • 時間複雜度:O(m)。在演算法的每一步都是搜尋關鍵詞的下一個字母,因此在最差的情況下,演算法需要執行m步。
  • 空間複雜度:O(1)。

(3)在trie中搜尋關鍵詞的字首

這個方法和我們在trie中用來搜尋關鍵詞的方法很類似。我們從根節點開始移動,直到關鍵詞字首的每個字母都被搜尋到了,或者,沒有辦法在trie中根據關鍵詞的當前字母找到接下去的路徑。這個方法和前面提到的搜尋關鍵詞的唯一不同在於,當我們遍歷到關鍵詞字首的最後一個字母時,我們總是返回true,我們不需要考慮當前的節點是否有結束標誌,因為我們只是搜尋關鍵詞的字首,而不是整個關鍵詞。

[翻譯]資料結構——trie樹介紹
圖9.在trie中搜尋關鍵詞字首

java編寫的搜尋關鍵詞字首的方法

class Trie {
    ...

    // Returns if there is any word in the trie
    // that starts with the given prefix.
    public boolean startsWith(String prefix) {
        TrieNode node = searchPrefix(prefix);
        return node != null;
    }
}
複製程式碼

複雜度分析:

  • 時間複雜度:O(m)
  • 空間複雜度:O(1)

3. 應用問題
這裡有一些非常合適應大家去練習的問題,這些問題都能用trie資料結構解決。

這篇分析出自@elmirap

相關文章