↓快轉到主要內容
  1. Algorithms/

Trie 字典樹

·2 分鐘· ·
演算法 Trie String Prefix Bit-Manipulation
Wei Yi Chung
作者
Wei Yi Chung
Working at the contributing of open source, distributed systems, and data engineering.
目錄

介紹
#

Trie(字典樹、前綴樹)把一堆字串的共同前綴合併在一起。從根節點往下走,每條邊是一個字元,走到的節點就代表一個前綴。

辨識訊號:

  • 大量字串要反覆做「前綴」相關的查詢:某個前綴出現幾次、有沒有以它開頭的單字。
  • 在網格或字串上同時找很多個單字:用 Trie 一次比對所有單字,而不是每個單字各搜一次。

模板一:用巢狀 dict 建 Trie
#

Python 最簡單的寫法是每個節點都是一個 dict:

root = {}
for word in words:
    node = root
    for ch in word:
        node = node.setdefault(ch, {})
    node["$"] = word                  # 結尾標記,順便存整個單字

主要參數說明:
#

  • setdefault(ch, {}):子節點不存在就建立,然後回傳它。
  • "$":結尾標記。存整個單字的好處是找到時不用再從路徑組回來(LC 212)。
  • 也可以用 class TrieNode 搭配 children 和 is_end,面試時比較清楚,但程式碼比較長。

模板二:在節點上記錄經過的次數
#

如果要知道「有幾個單字以這個前綴開頭」,就在每個節點記一個計數:

trie = {}
for word in words:
    node = trie
    for ch in word:
        if ch not in node:
            node[ch] = [0, {}]        # [經過次數, 子節點]
        node[ch][0] += 1
        node = node[ch][1]

查詢某個單字所有前綴的分數總和(LC 2416),就是沿路把計數加起來。

模板三:Trie + 網格 DFS
#

在網格上找很多單字時,DFS 的時候同時在 Trie 上往下走,Trie 裡沒有的方向就不用走:

def dfs(r, c, parent):
    ch = board[r][c]
    node = parent[ch]
    if "$" in node:
        res.append(node.pop("$"))     # 找到後移除,避免重複
    board[r][c] = "#"
    for 四個方向的 (nr, nc):
        if 在範圍內 and board[nr][nc] in node:
            dfs(nr, nc, node)
    board[r][c] = ch
    if not node:
        parent.pop(ch)                # 這個分支已經沒有單字了,剪掉

找到的單字從 Trie 移除,空的節點也一起刪掉,之後的搜尋就不會再走進這些死路。

什麼時候不需要 Trie
#

LeetCode 把一些題目標成 Trie,但其實有更簡單的做法:

題目更簡單的做法
LC 1023 Camelcase Matching每個 query 和 pattern 用雙指針比對,大寫字母必須對上
LC 1178 Number of Valid Words單字只在乎「用到哪些字母」,轉成 26-bit 遮罩,再枚舉謎題遮罩的子集合

判斷方式:如果查詢只跟「字母的集合」有關,不在乎順序,通常用位元遮罩更好。

複雜度
#

  • 建 Trie:O(所有單字的總長度)。
  • 查詢一個長度 L 的字串:O(L)。
  • 空間:最壞是 O(總長度 × 字母表大小);用 dict 只存實際出現的子節點,通常小很多。

常見錯誤
#

  • 用 class 實作時每個節點都開 26 格的陣列:單字少但字母分散時很浪費記憶體。
  • Trie + DFS 找到單字後沒有移除結尾標記:同一個單字會被重複加入答案。
  • 為了用 Trie 而用 Trie:只在乎字母集合的題目,位元遮罩更快。

相關 LeetCode
#

  • LC 212 Word Search II
  • LC 2416 Sum of Prefix Scores of Strings
  • LC 1023 Camelcase Matching
  • LC 1178 Number of Valid Words for Each Puzzle
  • LC 208 Implement Trie (Prefix Tree)

相關文章

Two Pointers 雙指針
·5 分鐘
演算法 Two-Pointers String Sorting
對撞指針、兩個序列各一個指針、跳過空白比對位置、排序後數 pair,以及原地覆寫的寫入指針
String DP
·2 分鐘
演算法 Dynamic-Programming String Lcs Palindrome
Prefix table、edit operations、subsequence 與 interval palindrome DP
LeetCode 2416: Sum of Prefix Scores of Strings
·1 分鐘
LeetCode Hard Trie String Counting
LeetCode 2416 解題筆記:Trie 每個節點記錄經過的單字數,單字的答案就是沿路計數的總和
LeetCode 1178: Number of Valid Words for Each Puzzle
·1 分鐘
LeetCode Hard Bit-Manipulation Hash-Map Trie
LeetCode 1178 解題筆記:單字只在乎用了哪些字母,轉成 26-bit 遮罩計數,再枚舉謎題遮罩的所有子集合
LeetCode 1023: Camelcase Matching
·1 分鐘
LeetCode Medium Two-Pointers String Trie
LeetCode 1023 解題筆記:雙指針逐字比對,pattern 以外多出來的只能是小寫字母
LeetCode 1763: Longest Nice Substring
·1 分鐘
LeetCode Easy Sliding-Window Divide-and-Conquer Hash-Table String Bit-Manipulation
LeetCode 1763 解題筆記:沒有大小寫配對的字元一定不在答案裡,用它切開字串遞迴;長度相同時取左邊的