🌳 字典树 — 前缀匹配的利器

自动补全、拼写检查、词频统计——字典树是字符串搜索的基石

📖 字典树的核心概念

字典树(Trie,又称前缀树)是一种树形数据结构,每条边代表一个字符,从根到某节点的路径对应一个字符串前缀。它特别适合处理字符串的前缀匹配问题,查找时间只与字符串长度有关,与集合大小无关。

Trie 存储 "app", "apple", "apply", "ban", "banana": root / \ a b | | p a | | p* n* / \ \ l l a | | | e* y* n | a* * = is_end (完整单词结尾) 特点: - 公共前缀共享节点,节省空间 - 查找 "app" 只需走3步,与词典大小无关 - 前缀查询: startsWith("app") → True

1. Trie vs 哈希表

Trie 的优势:
- 前缀匹配:哈希表无法高效查前缀,Trie 天然支持
- 空间效率:公共前缀共享节点,大量字符串时比哈希表省空间
- 有序性:Trie 的中序遍历即字典序
- 时间复杂度:查找 O(L),L为字符串长度,与集合大小无关
哈希表的优势:
- 精确匹配更快(O(1) vs O(L))
- 实现简单,Python dict 一行搞定
- 不需要频繁前缀查询时,哈希表更实用

2. Python Trie 模板

class TrieNode:
    def __init__(self):
        self.children = {}   # char → TrieNode
        self.is_end = False  # 是否单词结尾

class Trie:
    def __init__(self):
        self.root = TrieNode()
    
    def insert(self, word: str) -> None:
        node = self.root
        for ch in word:
            if ch not in node.children:
                node.children[ch] = TrieNode()
            node = node.children[ch]
        node.is_end = True
    
    def search(self, word: str) -> bool:
        node = self._find(word)
        return node is not None and node.is_end
    
    def startsWith(self, prefix: str) -> bool:
        return self._find(prefix) is not None
    
    def _find(self, prefix: str):
        """沿前缀走,返回终点节点或None"""
        node = self.root
        for ch in prefix:
            if ch not in node.children:
                return None
            node = node.children[ch]
        return node
Python 中用 dict 做 children 比 List[Optional[TrieNode]](26个位置)更省空间,因为不需要为不存在的字符分配空间。如果只处理小写字母,也可以用 26 长度的列表,速度略快。

🎯 题目一:实现 Trie (LC 208)

题目描述:实现 Trie 类,支持 insertsearchstartsWith 三个操作。

操作演示: insert("apple") search("apple") → True (完整匹配) search("app") → False (前缀但不是完整单词) startsWith("app")→ True (前缀存在) insert("app") search("app") → True (现在是完整单词了)

代码实现

class Trie:
    def __init__(self):
        self.children = {}
        self.is_end = False
    
    def insert(self, word: str) -> None:
        node = self
        for ch in word:
            if ch not in node.children:
                node.children[ch] = Trie()
            node = node.children[ch]
        node.is_end = True
    
    def search(self, word: str) -> bool:
        node = self._find(word)
        return node is not None and node.is_end
    
    def startsWith(self, prefix: str) -> bool:
        return self._find(prefix) is not None
    
    def _find(self, prefix: str):
        node = self
        for ch in prefix:
            if ch not in node.children:
                return None
            node = node.children[ch]
        return node

复杂度分析

insert 时间:O(L),search 时间:O(L),startsWith 时间:O(L)

空间复杂度:O(N×L),N为单词数,L为平均长度(公共前缀共享后更少)

🎯 题目二:添加与搜索单词 (LC 211)

题目描述:设计一个数据结构,支持添加单词和搜索单词。搜索时,. 可以匹配任意一个字母。

操作演示: addWord("bad") addWord("dad") addWord("mad") search("pad") → False (无p开头的词) search("bad") → True (精确匹配) search(".ad") → True (.匹配b/d/m) search("b..") → True (b.匹配bad)

思路分析

Trie + DFS:添加单词就是标准 Trie 插入。搜索时遇到 .,需要尝试所有子节点,用递归(DFS)处理。遇到普通字符,走对应的子节点;遇到 .,遍历所有子节点递归搜索。

代码实现

class WordDictionary:
    def __init__(self):
        self.children = {}
        self.is_end = False
    
    def addWord(self, word: str) -> None:
        node = self
        for ch in word:
            if ch not in node.children:
                node.children[ch] = WordDictionary()
            node = node.children[ch]
        node.is_end = True
    
    def search(self, word: str) -> bool:
        return self._search(word, 0)
    
    def _search(self, word: str, idx: int) -> bool:
        if idx == len(word):
            return self.is_end
        ch = word[idx]
        if ch == '.':
            # 通配符:尝试所有子节点
            return any(
                child._search(word, idx + 1) 
                for child in self.children.values()
            )
        if ch not in self.children:
            return False
        return self.children[ch]._search(word, idx + 1)

复杂度分析

addWord:O(L)

search 最坏:O(26^L)(全通配符),但实际远好于此

LC 211 的 search 中 . 的处理是考点。关键是不要把 . 当特殊字符插入 Trie,只在搜索时处理。面试官可能会问"如果通配符是 * 匹配零或多个字符呢?"那需要更复杂的回溯。

🎯 题目三:单词搜索 II (LC 212)

题目描述:给定 m×n 字符网格和单词列表,找出所有同时在网格和列表中出现的单词。单词由相邻格子(上下左右)的字母组成,同一格子不能重复使用。

示例: board = [["o","a","a","n"],["e","t","a","e"], ["i","h","k","r"],["i","f","l","v"]] words = ["oath","pea","eat","rain"] 网格: o a a n e t a e i h k r i f l v oath: o→a(右)→t(左下)→h(下) ✓ eat: e→a→t ✓ 结果: ["eat","oath"]

思路分析

Trie + DFS 回溯:将所有单词建 Trie,然后从网格每个格子出发做 DFS。沿路径匹配 Trie 节点,如果到达单词结尾就记录结果。关键优化:(1) 在 Trie 节点存完整单词,找到后清空避免重复;(2) 剪枝:如果当前字符不在 Trie 中,立即返回。

代码实现

class Solution:
    def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
        # 建 Trie,节点存完整单词
        class TrieNode:
            def __init__(self):
                self.children = {}
                self.word = None  # 存完整单词(非None表示是单词结尾)
        
        root = TrieNode()
        for word in words:
            node = root
            for ch in word:
                if ch not in node.children:
                    node.children[ch] = TrieNode()
                node = node.children[ch]
            node.word = word
        
        rows, cols = len(board), len(board[0])
        result = []
        
        def dfs(r, c, node):
            ch = board[r][c]
            if ch not in node.children:
                return
            child = node.children[ch]
            if child.word:
                result.append(child.word)
                child.word = None  # 避免重复添加
            
            board[r][c] = '#'  # 标记已访问
            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 and board[nr][nc] != '#':
                    dfs(nr, nc, child)
            board[r][c] = ch  # 回溯
        
        for r in range(rows):
            for c in range(cols):
                dfs(r, c, root)
        
        return result

复杂度分析

时间复杂度:O(m×n×4×3^(L-1)),L为最长单词长度

空间复杂度:O(总字符数),Trie 空间

LC 212 的关键优化:找到单词后 child.word = None,这样同一个单词不会被重复添加,而且如果该节点没有子节点,后续路径也不会再走到这里,起到剪枝效果。

🔬 Trie 的高级应用

1. 前缀计数与词频统计

在 TrieNode 中增加 count 字段,插入时路径上每个节点 count+1,可以统计每个前缀出现次数。

class TrieNode:
    def __init__(self):
        self.children = {}
        self.count = 0   # 经过此节点的单词数
        self.is_end = 0  # 以此节点结尾的单词数

# 插入时: node.count += 1, 最后 node.is_end += 1
# 查前缀频率: 沿前缀走到节点, 返回 node.count

2. Trie 与异或问题

将数字的二进制表示插入 01-Trie,可以在 O(32) 时间内找到与目标数异或值最大的数。这是 LC 421 和 LC 1707 的核心解法。

# 01-Trie: 每个节点只有 0 和 1 两个子节点
class BitTrie:
    def __init__(self):
        self.children = [None, None]  # [bit0, bit1]

# 查最大异或: 尽量走相反位
# 如果目标位是0, 优先走1; 如果目标位是1, 优先走0

3. 后缀自动机 vs Trie

Trie 处理前缀,后缀自动机处理子串。如果问题涉及子串匹配而非前缀匹配,Trie 可能不是最佳选择。

面试中 Trie 的常见考法:LC 208(基础实现)、LC 211(通配符搜索)、LC 212(网格搜索+Trie)、LC 421(01-Trie异或)。掌握这四种模式,Trie 题基本通关。
成就解锁:前缀猎手 — 掌握Trie标准实现、通配符搜索、Trie+回溯综合应用三大核心技能
LeetCode AC验证:LC 208 Implement Trie ✅ | LC 211 Word Dictionary ✅ | LC 212 Word Search II ✅

📝 课后练习

  1. LC 421 数组中两个数的最大异或值(01-Trie)
  2. LC 648 单词替换(Trie前缀替换)
  3. LC 677 键值映射(Trie+求和)
  4. LC 720 词典中最长的单词(Trie+排序)
  5. LC 1707 与数组中元素的最大异或值(01-Trie+离线查询)

🔑 本课要点回顾

📚 扩展阅读

思考题:如果 Trie 中的字符集非常大(比如 Unicode 全字符),应该如何优化空间?提示:可以用三元搜索树(TST)替代标准 Trie,每个节点存储一个字符和左右中三个指针,类似 BST 的扩展。