0Pricing
DSA Interview Prep · 课时

Trie 中的通配符与正则搜索

通过在相应深度扩展到所有子节点来支持“.”通配符匹配,解决设计添加和搜索单词数据结构问题。

Trie 中的通配符与正则搜索 是 CoddyKit 上的免费 DSA Interview Prep 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 DSA Interview Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 DSA Interview Prep 课程共包含 4 节课。

通配符搜索问题

标准字典树的 search 处理精确字符。通配符搜索增加了特殊字符 '.',它可以匹配任意单个字符。在 search 过程中遇到 '.' 时,不能再沿着某一个特定子节点继续,而必须尝试所有子节点,也就是进行分支扩展。这正是 LeetCode 211“设计添加和搜索单词的数据结构”的核心思想。每个 '.' 都会按照该层的子节点数量成倍增加搜索路径。

递归通配符搜索

使用递归的 DFS 辅助函数实现通配符搜索。对于模式中的每个字符:如果它是字面字符,就沿着对应的子节点继续(如果子节点不存在则返回假值);如果它是 '.',就递归尝试所有子节点,只要有一个成功就返回真值。模式处理完毕后,返回 node.is_end。

class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_end = False

class WordDictionary:
    def __init__(self):
        self.root = TrieNode()
    
    def addWord(self, word):
        node = self.root
        for c in word:
            if c not in node.children:
                node.children[c] = TrieNode()
            node = node.children[c]
        node.is_end = True
    
    def search(self, word):
        def dfs(node, i):
            if i == len(word):
                return node.is_end
            c = word[i]
            if c == '.':
                return any(dfs(child, i+1) for child in node.children.values())
            if c not in node.children:
                return False
            return dfs(node.children[c], i+1)
        return dfs(self.root, 0)

wd = WordDictionary()
wd.addWord('bad')
wd.addWord('dad')
wd.addWord('mad')
print(wd.search('.ad'))  # True
print(wd.search('b..'))  # True
print(wd.search('pad'))  # False

为何使用任一判断进行分支扩展

遇到 '.' 时,我们调用 any(dfs(child, i+1) for child in node.children.values())。这个用于判断任一结果的生成器具有短路特性:只要某个子节点返回真值,它就会立即停止。这样可以避免不必要的探索。在最坏情况下(模式完全由 '.' 组成),我们必须探索所有路径,复杂度为 O(26^k),其中 k 是点号的数量,因此对于大型字典树,像 '....' 这样的模式开销很大。

使用队列进行迭代式通配符搜索

迭代方法使用一个由 (node, index) 对组成的队列,从 (root, 0) 开始。对于每一对元素,如果 index == len(word) 且 node.is_end,就返回真值。否则,处理当前字符:如果是 '.',则将所有子节点加入队列;如果是字面字符,则只将匹配的子节点加入队列。这本质上是在字典树路径上执行 BFS。

from collections import deque

def search_iterative(root, word):
    queue = deque([(root, 0)])
    while queue:
        node, i = queue.popleft()
        if i == len(word):
            if node.is_end:
                return True
            continue
        c = word[i]
        if c == '.':
            for child in node.children.values():
                queue.append((child, i+1))
        elif c in node.children:
            queue.append((node.children[c], i+1))
    return False

print('Iterative BFS-based wildcard search')

通配符搜索的复杂度分析

对于不含通配符的模式,search 的复杂度为 O(m)。对于包含 k 个通配符的模式,最坏情况为 O(26^k × m),也就是关于通配符数量呈指数增长。实际应用中,通配符通常比较稀疏,而且字典树的深度较小,因此性能通常可以接受。对于完全由通配符组成的模式(例如匹配长度为 k 的所有单词),算法会退化为完整的字典树遍历。

超越单字符通配符的正则表达式搜索

扩展到完整的正则表达式(例如使用 '*' 匹配零个或多个字符)需要采用不同的处理方式。'*' 可以匹配任意后缀,因此遇到它时,必须从当前节点开始尝试所有字典树路径。在字典树中实现真正的正则表达式匹配较为复杂,通常需要使用 NFA/DFA 构造。面试中,单字符通配符('.')通常才是标准模式。

通配模式匹配

使用 '?'(任意单个字符)和 '*'(包括空序列在内的任意序列)进行通配模式匹配时,可以使用 DP 实现。如果在字典树中实现,'?' 对应单层分支扩展(类似 '.'),而 '*' 对应多层 DFS。组合 DP 方法定义为:dp[i][j] = 当且仅当 pattern[0..i] 匹配 string[0..j] 时为真值。面试官通常会明确要求实现哪一种变体。

实际应用:IP 地址路由

通配符字典树用于IP 路由表,其中 '*' 充当前缀通配符。路由器会存储类似 '192.168.*' 的路由前缀,并匹配传入的地址。最长前缀匹配(即最具体的路由优先)通过尽可能深入地遍历字典树,并使用最后一次发现的匹配来实现。这是字典树前缀操作和通配符操作在现实世界中的应用。

优化:剪枝无效分支

当字典树节点没有子节点(即叶节点),且结束标记为假值时,任何到达该节点的 search 操作都会返回假值。在通配符搜索过程中,如果在递归前跳过这些无效节点,就可以剪掉不必要的调用。在每个节点中维护子树包含的单词总数,还可以在没有单词符合剩余模式长度限制时,跳过整个子树。

完整的 WordDictionary 类(面试可用)

这是一个简洁、适合面试的 WordDictionary 类,将插入和点号通配符搜索结合在一起。这正是 LeetCode 211 所要求的实现。递归 search 配合具有短路特性的任一判断,代码简洁,并且能清晰展示分支扩展逻辑。

class WordDictionary:
    def __init__(self):
        self.root = {}
    
    def addWord(self, word):
        node = self.root
        for c in word:
            node = node.setdefault(c, {})
        node['#'] = True
    
    def search(self, word):
        def dfs(node, i):
            if i == len(word):
                return '#' in node
            if word[i] == '.':
                return any(dfs(v, i+1) for k, v in node.items() if k != '#')
            nxt = node.get(word[i])
            return dfs(nxt, i+1) if nxt is not None else False
        return dfs(self.root, 0)

wd = WordDictionary()
for w in ['at','and','an','add']:
    wd.addWord(w)
print(wd.search('a.'))   # True (at, an)
print(wd.search('.nd'))  # True (and)
print(wd.search('...'))  # True (and, add)
print(wd.search('x.'))   # False

使用 setdefault 构建紧凑字典树

dict.setdefault(key, default) 在键存在时返回该键对应的值,否则插入 default 并返回它。在插入操作中使用 node.setdefault(c, {}) 可以省去 if-else 检查:如果子节点字典不存在,就创建它;无论是否新建,都会返回该字典。这样,插入操作就可以写成单行遍历:for c in word: node = node.setdefault(c, {})。代码简洁,也符合该语言的惯用风格。

快速检查

检验您对本课数据结构与算法——编程面试准备相关概念的理解。

课程回顾

本课您学习了:通配符 '.' 需要在匹配位置使用递归 DFS 扩展到所有子节点;生成器配合任一判断可以通过短路求值提前终止;以及setdefault 可以用单行代码完成紧凑的字典树插入。接下来,我们将结合字典树和回溯,解决单词搜索 II——在二维棋盘上同时查找多个单词。

常见问题解答

「Trie 中的通配符与正则搜索」课时是免费的吗?

是的 — 「Trie 中的通配符与正则搜索」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 DSA Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 DSA Interview Prep 课程共包含 4 节课。

「Trie 中的通配符与正则搜索」这节课中我会学到什么?

通过在相应深度扩展到所有子节点来支持“.”通配符匹配,解决设计添加和搜索单词数据结构问题。 你通过在浏览器中直接运行的动手代码来练习 DSA Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 DSA Interview Prep 需要有经验吗?

无需任何先前经验。CoddyKit 上的 DSA Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「Trie 中的通配符与正则搜索」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 DSA Interview Prep 课中编写并运行代码吗?

能。每节 DSA Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. TrieNode 类:插入与搜索
  2. 前缀搜索与 Starts-With
  3. Trie 中的通配符与正则搜索
  4. 单词搜索 II:Trie + 网格回溯
← 返回 DSA Interview Prep