1. 程式人生 > 其它 >淺談字典樹 + LeetCode——720. 詞典中最長的單詞(Java)

淺談字典樹 + LeetCode——720. 詞典中最長的單詞(Java)

引言

字首樹,也叫字典樹,我們成為 Trie樹(發音類似 "try"),是一種多路樹形結構,是雜湊樹的一種延伸。

效率方面與hash樹差不多,也是一種快速檢索的多叉樹,用於統計和排序大量的字串,經常用於搜尋引擎的文字詞頻統計。

最大的優點就是減少無用的字串比較,查詢速度快,核心思想就是用空間換時間,利用查詢儲存的公共字首降低時間開銷,

這樣缺點也很明顯,因為需要提前定義儲存各種情況的公共字首,所以記憶體開銷非常大。

實現字首樹

字首樹是一棵有根樹,其每個節點包含以下欄位:
  指向子節點的指標陣列 children。對於實際情況而言,陣列長度訂為 26,即小寫英文字母的數量。
  此時 children[0] 對應小寫字母 a,children[1] 對應小寫字母 b,…,children[25] 對應小寫字母 z。
  布林欄位 isEnd,表示該節點是否為字串的結尾。

插入字串(insert)
  我們從字典樹的根開始,插入字串。對於當前字元對應的子節點,有兩種情況:
  子節點存在。沿著指標移動到子節點,繼續處理下一個字元。
  子節點不存在。建立一個新的子節點,記錄在 children 陣列的對應位置上,然後沿著指標移動到子節點,繼續搜尋下一個字元。
  重複以上步驟,直到處理字串的最後一個字元,然後將當前節點標記為字串的結尾。

查詢字首(search)
  我們從字典樹的根開始,查詢字首。對於當前字元對應的子節點,有兩種情況:
  子節點存在。沿著指標移動到子節點,繼續搜尋下一個字元。
  子節點不存在。說明字典樹中不包含該字首,返回空指標。
  重複以上步驟,直到返回空指標或搜尋完字首的最後一個字元。

若搜尋到了字首的末尾,就說明字典樹中存在該字首。
此外,若字首末尾對應節點的 isEnd 為真,則說明字典樹中存在該字串。
class Trie {
    private final Trie[] children;
    private boolean isEnd;
    
    // 初始化
    public Trie() {
        children = new Trie[26];
        isEnd = false;
    }
    
    // 插入新元素
    public void insert(String word) {
        Trie node = this;
        for (int i = 0; i < word.length(); i++) {
            char ch = word.charAt(i);
            int index = ch - 'a';

            if (node.children[index] == null) {
                node.children[index] = new Trie();
            }
            node = node.children[index];
        }
        node.isEnd = true;
    }
    
    // 查詢元素是否存在
    public boolean search(String word) {
        Trie node = searchPrefix(word);
        return node != null && node.isEnd;
    }
   
    // 查詢字首是否存在
    private Trie searchPrefix(String prefix) {
        Trie node = this;
        for (int i = 0; i < prefix.length(); i++) {
            char ch = prefix.charAt(i);
            int index = ch - 'a';
            if (node.children[index] == null) {
                return null;
            }
            node = node.children[index];
        }
        return node;
    }
}

題目描述

題幹:
  給出一個字串陣列 words 組成的一本英語詞典。
  返回 words 中最長的一個單詞,該單詞是由 words 詞典中其他單詞逐步新增一個字母組成。
  若其中有多個可行的答案,則返回答案中字典序最小的單詞。若無答案,則返回空字串。

示例 1:
  輸入:words = ["w","wo","wor","worl", "world"]
  輸出:"world"
  解釋: 單詞"world"可由"w", "wo", "wor", 和 "worl"逐步新增一個字母組成。

示例 2:
  輸入:words = ["a", "banana", "app", "appl", "ap", "apply", "apple"]
  輸出:"apple"
  解釋:"apply" 和 "apple" 都能由詞典中的單片語成。但是 "apple" 的字典序小於 "apply" 

題解思路

這裡採用字首樹最明顯的提示就是該單詞由其他單片語成,這樣用字首樹模型只需判斷序號即可。

如果不採用字首樹的方法,直接用雜湊表儲存來代替也可以實現,而且速度上也相差不多,

這樣就印證了開頭我們所說的效率問題,具體思路還是陣列的排序和遍歷,排序之後保證長度和序號正確,

之後無論是用字首樹依次新增還是用雜湊表儲存出現過的單詞判斷當前遍歷的單詞是否由其他的單片語成皆可。
    public String longestWord(String[] words) {
        Arrays.sort(words, (a, b) -> {
            if (a.length() != b.length()) {
                return a.length() - b.length();
            } else {
                return b.compareTo(a);
            }
        });

        String longest = "";
        Set<String> set = new HashSet<>();
        set.add("");
        for (String word : words) {
            if (set.contains(word.substring(0, word.length() - 1))) {
                set.add(word);
                longest = word;
            }
        }
        return longest;
    }

    public String longestWord01(String[] words) {
        Trie trie = new Trie();
        for (String word : words) {
            trie.insert(word);
        }
        String longest = "";
        for (String word : words) {
            if (trie.search(word)) {
                if (word.length() > longest.length() || (word.length() == longest.length() && word.compareTo(longest) < 0)) {
                    longest = word;
                }
            }
        }
        return longest;
    }

總結

雖然題目上有點明顯展示字首樹的嫌疑,不過確實是字首樹的經典例題,能夠加深對字首樹的理解和感受。

當然有人會覺得這裡的字首樹和雜湊表過於浪費空間,所以可以用Stack判斷往裡pop和push。

如果文章存在問題歡迎在評論區斧正和評論,各自努力,你我最高處見。