數(shù)據(jù)結(jié)構(gòu)之Trie字典樹(shù)

什么是Trie字典樹(shù)

Trie 樹(shù),也叫“字典樹(shù)”或“前綴樹(shù)”。顧名思義,它是一個(gè)樹(shù)形結(jié)構(gòu)。但與二分搜索樹(shù)、紅黑樹(shù)等不同的是,Trie 樹(shù)是一種多叉樹(shù),即每個(gè)節(jié)點(diǎn)可以有 m 個(gè)子節(jié)點(diǎn)。它是一種專門(mén)處理字符串匹配的數(shù)據(jù)結(jié)構(gòu),用來(lái)解決在一組字符串集合中快速查找某個(gè)字符串的問(wèn)題。

例如,在一個(gè)字典中有 n 個(gè)條目,如果使用普通的二分搜索樹(shù)(不考慮退化),那么在該字典中查詢指定條目的時(shí)間復(fù)雜度是 O(logn),如果有100w個(gè)條目(2^{20}),logn 大約為20。

而如果使用 Trie 樹(shù)的話,查詢每個(gè)條目的時(shí)間復(fù)雜度,和字典中一共有多少條目無(wú)關(guān)。時(shí)間復(fù)雜度為 O(w),其中 w 為查詢單詞的長(zhǎng)度,而且絕大多數(shù)的單詞長(zhǎng)度都小于 10。由此可見(jiàn),使用 Trie 樹(shù)實(shí)現(xiàn)字符串查詢,特別是只查詢其前綴的情況下,是比普通的樹(shù)形結(jié)構(gòu)效率要更高的。

那么 Trie 樹(shù)是如何做到其查詢時(shí)間復(fù)雜度與條目數(shù)量無(wú)關(guān)的呢?這是因?yàn)?Trie 樹(shù)的本質(zhì),就是利用字符串之間的公共前綴,將重復(fù)的前綴合并在一起。例如,我們將:how,hi,her,hello,so,see 這6個(gè)字符串構(gòu)造成一顆 Trie 樹(shù)。那么,最后構(gòu)造出來(lái)的就是下面這個(gè)圖中的樣子:


image.png

其中,根節(jié)點(diǎn)不包含任何信息。每個(gè)節(jié)點(diǎn)表示一個(gè)字符串中的字符,從根節(jié)點(diǎn)到紅色節(jié)點(diǎn)的一條路徑表示一個(gè)字符串(注意:紅色節(jié)點(diǎn)并不都是葉子節(jié)點(diǎn))。

為了更容易理解 Trie 樹(shù)是怎么構(gòu)造出來(lái)的,我們可以看如下 Trie 樹(shù)構(gòu)造的分解過(guò)程。構(gòu)造過(guò)程的每一步,都相當(dāng)于往 Trie 樹(shù)中插入一個(gè)字符串。當(dāng)所有字符串都插入完成之后,Trie 樹(shù)就構(gòu)造好了:


image.png

image.png

當(dāng)我們?cè)?Trie 樹(shù)中查找一個(gè)字符串的時(shí)候,比如查找字符串“her”,那我們將要查找的字符串分割成單個(gè)的字符 h,e,r,然后從 Trie 樹(shù)的根節(jié)點(diǎn)開(kāi)始匹配。如圖所示,綠色的路徑就是在 Trie 樹(shù)中匹配的路徑:


image.png

之前有提到過(guò), Trie 樹(shù)是多叉樹(shù),那么這個(gè)“多叉”是怎么體現(xiàn)的呢?通常來(lái)講,如果你只針對(duì)小寫(xiě)字母構(gòu)造一棵 Trie 樹(shù),就像我們上面的例子,那么每個(gè)節(jié)點(diǎn)中使用一個(gè)長(zhǎng)度為26的數(shù)組來(lái)表示其多個(gè)子節(jié)點(diǎn)即可。如下所示:

class Node {
    char data;
    Node children[26];
}

而如果我們的需求不僅僅是只包含小寫(xiě)字母,希望這是一棵通用的 Trie 樹(shù),那么就需要設(shè)計(jì)一個(gè)能動(dòng)態(tài)變化的子節(jié)點(diǎn)容器,使得每個(gè)節(jié)點(diǎn)有若干指向下個(gè)節(jié)點(diǎn)的指針。例如,我們可以使用一個(gè) Map 來(lái)實(shí)現(xiàn),如下所示:

class Node {
    boolean isWord;  // 標(biāo)識(shí)是否是單詞的結(jié)尾
    Map<Character, Node> next;
}

Trie字典樹(shù)基礎(chǔ)代碼

通過(guò)以上的介紹,我們已經(jīng)了解到了 Trie 樹(shù)的基本概念。接下來(lái),讓我們實(shí)現(xiàn)一下 Trie 樹(shù)的基礎(chǔ)功能代碼,從代碼上對(duì) Trie 樹(shù)有個(gè)直觀的認(rèn)識(shí)。具體代碼如下:

package tree;

import java.util.Map;
import java.util.TreeMap;

/**
 * Trie樹(shù)
 *
 * @author 01
 * @date 2021-01-28
 **/
public class TrieTree {

    private final Node root;

    private int size;

    /**
     * Trie樹(shù)中每個(gè)節(jié)點(diǎn)的結(jié)構(gòu)
     */
    private static class Node {
        /**
         * 標(biāo)識(shí)是否是單詞的結(jié)尾
         */
        private boolean isWord;

        /**
         * 使用Map來(lái)實(shí)現(xiàn)動(dòng)態(tài)存儲(chǔ)多個(gè)子節(jié)點(diǎn)
         */
        private final Map<Character, Node> next;

        public Node(boolean isWord) {
            this.isWord = isWord;
            next = new TreeMap<>();
        }

        public Node() {
            this(false);
        }
    }

    public TrieTree() {
        root = new Node();
        size = 0;
    }

    /**
     * 獲取Trie中存儲(chǔ)的單詞數(shù)量
     */
    public int getSize() {
        return size;
    }

    /**
     * 向Trie中添加一個(gè)新的單詞word
     */
    public void add(String word) {
        Node current = root;
        for (int i = 0; i < word.length(); i++) {
            char c = word.charAt(i);
            if (current.next.get(c) == null) {
                // 沒(méi)有與之對(duì)應(yīng)的子節(jié)點(diǎn),創(chuàng)建一個(gè)新的子節(jié)點(diǎn)
                current.next.put(c, new Node());
            }
            current = current.next.get(c);
        }

        if (!current.isWord) {
            // 添加的是新的單詞,標(biāo)識(shí)該節(jié)點(diǎn)是單詞的結(jié)尾
            current.isWord = true;
            size++;
        }
    }
}

Trie字典樹(shù)的查詢

Trie 字典樹(shù)的查詢主要就是查詢某個(gè)單詞是否存在于 Trie 中,其主要邏輯與 add 方法基本上是一樣的。代碼如下:

/**
 * 查詢單詞word是否在Trie中
 */
public boolean contains(String word){
    Node current = root;
    for (int i = 0; i < word.length(); i++) {
        char c = word.charAt(i);
        if (current.next.get(c) == null) {
            return false;
        }
        current = current.next.get(c);
    }

    // 只有當(dāng)最后一個(gè)字母所對(duì)應(yīng)的節(jié)點(diǎn)標(biāo)識(shí)了是一個(gè)單詞的結(jié)尾,
    // 才能認(rèn)為這個(gè)單詞存在于Trie中
    return current.isWord;
}

Trie字典樹(shù)的前綴查詢

相比于查詢某個(gè)單詞是否存在 Trie 樹(shù)中,前綴查詢的使用范圍更廣,也是 Trie 樹(shù)中的主要查詢操作。通過(guò)前綴查詢,我們可以實(shí)現(xiàn)像搜索引擎那樣的搜索關(guān)鍵詞提示功能。實(shí)現(xiàn)前綴查詢的代碼與查詢某個(gè)單詞基本上是一樣的,如下所示:

/**
 * 查詢是否在Trie中有單詞以prefix為前綴
 */
public boolean hasPrefix(String prefix) {
    Node current = root;
    for (int i = 0; i < prefix.length(); i++) {
        char c = prefix.charAt(i);
        if (current.next.get(c) == null) {
            return false;
        }
        current = current.next.get(c);
    }

    return true;
}

Trie字典樹(shù)和簡(jiǎn)單的模式匹配

接下來(lái),我們嘗試使用Trie字典樹(shù)來(lái)解決LeetCode上的一個(gè)簡(jiǎn)單模式匹配的問(wèn)題,該問(wèn)題的編號(hào)是211:

關(guān)于這個(gè)問(wèn)題的詳細(xì)內(nèi)容,可以查看以上鏈接,這里就不做贅述了。對(duì)于該問(wèn)題,具體的實(shí)現(xiàn)代碼如下:

package tree.trie;

import java.util.Map;
import java.util.TreeMap;

/**
 * Leetcode 211. Add and Search Word - Data structure design
 * https://leetcode.com/problems/add-and-search-word-data-structure-design/description/
 *
 * @author 01
 */
public class WordDictionary {

    private static class Node {

        private boolean isWord;
        private final Map<Character, Node> next;

        public Node(boolean isWord) {
            this.isWord = isWord;
            next = new TreeMap<>();
        }

        public Node() {
            this(false);
        }
    }

    private final Node root;

    /**
     * Initialize your data structure here.
     */
    public WordDictionary() {
        root = new Node();
    }

    /**
     * Adds a word into the data structure.
     */
    public void addWord(String word) {
        Node cur = root;
        for (int i = 0; i < word.length(); i++) {
            char c = word.charAt(i);
            if (cur.next.get(c) == null) {
                cur.next.put(c, new Node());
            }
            cur = cur.next.get(c);
        }
        cur.isWord = true;
    }

    /**
     * Returns if the word is in the data structure.
     * A word could contain the dot character '.' to represent any one letter.
     */
    public boolean search(String word) {
        return match(root, word, 0);
    }

    private boolean match(Node node, String word, int index) {
        // 遞歸到底了,返回該節(jié)點(diǎn)是否是一個(gè)單詞
        if (index == word.length()) {
            return node.isWord;
        }

        char c = word.charAt(index);
        if (c != '.') {
            if (node.next.get(c) == null) {
                return false;
            }

            // 遞歸繼續(xù)匹配下一個(gè)字母
            return match(node.next.get(c), word, index + 1);
        } else {
            // 包含通配符,需要遍歷匹配全部子節(jié)點(diǎn)
            for (char nextChar : node.next.keySet()) {
                if (match(node.next.get(nextChar), word, index + 1)) {
                    return true;
                }
            }

            return false;
        }
    }
}

Trie字典樹(shù)和字符串映射

最后,我們?cè)賮?lái)解決一個(gè)LeetCode上的677號(hào)問(wèn)題,該問(wèn)題的鏈接如下:

對(duì)于該問(wèn)題我們就是要將Trie字典樹(shù)作為一個(gè)映射,每個(gè)單詞就是一個(gè) key,對(duì)應(yīng)著一個(gè) value,該 value 只存在于單詞最后一個(gè)字母對(duì)應(yīng)的節(jié)點(diǎn)。如下圖所示:

image.png

有了這個(gè)形象的概念后,代碼編寫(xiě)起來(lái)就簡(jiǎn)單了,所以也建議各位實(shí)現(xiàn)算法和數(shù)據(jù)結(jié)構(gòu)時(shí)可以嘗試多畫(huà)圖。對(duì)于該問(wèn)題的具體實(shí)現(xiàn)代碼如下:

package tree.trie;

import java.util.Map;
import java.util.TreeMap;

/**
 * 鍵值映射
 * https://leetcode-cn.com/problems/map-sum-pairs/
 *
 * @author 01
 */
public class MapSum {

    private static class Node {

        private int value;
        private final Map<Character, Node> next;

        public Node(int value) {
            this.value = value;
            next = new TreeMap<>();
        }

        public Node() {
            this(0);
        }
    }

    private final Node root;

    /**
     * Initialize your data structure here.
     */
    public MapSum() {
        root = new Node();
    }

    public void insert(String key, int val) {
        Node cur = root;
        for (int i = 0; i < key.length(); i++) {
            char c = key.charAt(i);
            if (cur.next.get(c) == null) {
                cur.next.put(c, new Node());
            }
            cur = cur.next.get(c);
        }
        cur.value = val;
    }

    public int sum(String prefix) {
        Node cur = root;
        // 找到這個(gè)前綴最后一個(gè)字母所對(duì)應(yīng)的節(jié)點(diǎn)
        for (int i = 0; i < prefix.length(); i++) {
            char c = prefix.charAt(i);
            if (cur.next.get(c) == null) {
                return 0;
            }
            cur = cur.next.get(c);
        }

        // 對(duì)該節(jié)點(diǎn)所有路徑下的子節(jié)點(diǎn)的value進(jìn)行求和
        return sum(cur);
    }

    private int sum(Node node) {
        int res = node.value;
        // 遍歷所有子節(jié)點(diǎn)
        for (char c : node.next.keySet()) {
            // 對(duì)每個(gè)子節(jié)點(diǎn)路徑上的value進(jìn)行遞歸求和
            res += sum(node.next.get(c));
        }

        return res;
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容