字典树(Trie)

字典树(Trie),又称前缀树,让字符串检索达到 O(m) 时间复杂度,m 为字符串长度。

什么是字典树?

字典树是一种树形数据结构,用于高效存储和检索字符串集合。核心思想:共享前缀,节省空间

示例:["apple", "app", "apply", "ban", "band"]

        root
       /    \
      a      b
     / \      \
    p   p      a
   / \   \     |
  p   l   l    n
  |   |   |    |
  l   y   y    d
  |         (apple, app, apply, ban, band)

所有以 “app” 开头的单词共享同一条路径,大大节省存储空间。


字典树的核心操作

1. 基本结构

class TrieNode:
    def __init__(self):
        self.children = {}      # 子节点映射 {char: TrieNode}
        self.is_end = False     # 是否是单词结尾
        self.word = None        # 结尾的完整单词(可选)
        
class Trie:
    def __init__(self):
        self.root = TrieNode()
    
    # 插入
    def insert(self, word: str):
        node = self.root
        for char in word:
            if char not in node.children:
                node.children[char] = TrieNode()
            node = node.children[char]
        node.is_end = True
        node.word = word
    
    # 搜索
    def search(self, word: str) -> bool:
        node = self._find_node(word)
        return node is not None and node.is_end
    
    # 前缀匹配
    def startsWith(self, prefix: str) -> bool:
        return self._find_node(prefix) is not None
    
    def _find_node(self, prefix: str):
        node = self.root
        for char in prefix:
            if char not in node.children:
                return None
            node = node.children[char]
        return node

2. 时间复杂度

操作 时间复杂度 空间复杂度
插入 O(m) O(m)
搜索 O(m) O(1)
前缀匹配 O(m) O(1)

m = 字符串长度

优势:相比哈希表,字典树可以高效处理前缀查询


LeetCode 实战

题目 1:实现 Trie(前缀树)

LeetCode 208 - 实现一个 Trie,支持 insert、search、startsWith 操作。

class Trie:

    def __init__(self):
        self.root = {}
        
    def insert(self, word: str) -> None:
        node = self.root
        for ch in word:
            if ch not in node:
                node[ch] = {}
            node = node[ch]
        node['#'] = True   # 标记单词结尾
        
    def search(self, word: str) -> bool:
        node = self._traverse(word)
        return node is not None and '#' in node
        
    def startsWith(self, prefix: str) -> bool:
        return self._traverse(prefix) is not None
    
    def _traverse(self, prefix: str):
        node = self.root
        for ch in prefix:
            if ch not in node:
                return None
            node = node[ch]
        return node

题目 2:单词搜索 II

LeetCode 212 - 在二维字符网格中找出所有存在于字典里的单词。

class Solution:
    def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
        # 构建 Trie
        trie = {}
        for word in words:
            node = trie
            for ch in word:
                node = node.setdefault(ch, {})
            node['#'] = word
        
        result = []
        rows, cols = len(board), len(board[0])
        
        def dfs(r, c, node):
            ch = board[r][c]
            if ch not in node:
                return
            
            next_node = node[ch]
            
            # 找到完整单词
            if '#' in next_node:
                result.append(next_node['#'])
                del next_node['#']  # 避免重复
            
            # 标记已访问
            board[r][c] = '#'
            
            # 四个方向 DFS
            for dr, dc in [(0,1), (0,-1), (1,0), (-1,0)]:
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols:
                    dfs(nr, nc, next_node)
            
            board[r][c] = ch  # 回溯
            
            # 剪枝:叶节点可删除
            if not next_node:
                del node[ch]
        
        for r in range(rows):
            for c in range(cols):
                if board[r][c] in trie:
                    dfs(r, c, trie)
        
        return result

优化要点

  • 用字典而非固定数组,更省空间
  • 找到单词后删除叶节点,实现剪枝
  • 回溯恢复字符状态

前缀树的应用场景

1. 自动补全 / 输入提示

搜索引擎、IDE 代码补全:

# 模拟搜索框前缀匹配
def autocomplete(trie, prefix, top_k=5):
    node = trie._find_node(prefix)
    if not node:
        return []
    
    # DFS 收集所有以 prefix 开头的单词
    results = []
    def dfs(n, path):
        if len(results) >= top_k:
            return
        if '#' in n:
            results.append(path)
        for ch, child in n.items():
            if ch != '#':
                dfs(child, path + ch)
    
    dfs(node, prefix)
    return results

2. IP 路由最长前缀匹配

网络路由表使用 Trie 进行最长前缀匹配。

3. 字符串排序

字典树的中序遍历即为按字典序排列的字符串序列。

4. 敏感词过滤

将敏感词库构建为 Trie,扫描文本时 O(n) 时间复杂度完成匹配。


字典树的变体

1. 压缩字典树(Compressed Trie)

将单字符节点压缩为字符串节点,减少节点数量:

压缩前:a → b → c → d
压缩后:abcd(单个节点存储字符串)

2. 后缀树(Suffix Tree)

存储字符串所有后缀,用于子串查询、模式匹配等问题。

3. 双数组字典树(Double-Array Trie)

用两个数组实现,兼顾速度和空间,是工业级词典分词的核心结构。


💡 小结

字典树的核心价值:

特性 说明
前缀共享 节省空间,适合字符串集合
O(m) 查找 m = 字符串长度,与规模无关
天然支持前缀查询 哈希表做不到
可扩展性强 轻松添加删除、统计词频

适用场景:前缀匹配、自动补全、敏感词过滤、词典分词。

面试高频:LeetCode 208(实现 Trie)、212(单词搜索 II)、820(单词压缩)。


相关阅读:并查集——城市连接问题