多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

设计支持通配符搜索的单词数据结构:Trie + DFS 实战解析(LeetCode 211 / Add and Search Word)

设计支持通配符搜索的单词数据结构:Trie + DFS 实战解析(LeetCode 211 / Add and Search Word) 设计支持通配符搜索的单词数据结构Trie DFS 实战解析LeetCode 211 / Add and Search Word【免费下载链接】leetcodeLeetcode solutions项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode本篇围绕 LeetCode 211「Design Add and Search Words Data Structure」展开以仓库内 hints/design-word-search-data-structure.md 的提示为核心骨架结合 articles/design-word-search-data-structure.md 的完整题解与各语言源码讲解如何用 Trie前缀树 DFS 实现一个支持.通配符的单词字典。读完你将掌握暴力解法的局限、Trie 的插入与递归搜索实现、通配符分支的剪枝思路以及这类「字典 模糊匹配」场景的通用解法。问题定义与前置知识LeetCode 211要求设计一个数据结构支持两类操作addWord(word)向字典中添加一个单词search(word)查询某个模式是否存在于字典中其中模式串可以包含..可以匹配任意一个小写字母。从仓库提示文档 hints/design-word-search-data-structure.md 可以明确看到这道题的目标复杂度每个函数调用 O(n) 时间、O(t n) 空间其中n是字符串长度t是 Trie 中创建的节点总数。动手之前需要具备三项基础Trie前缀树用于高效存储与检索字符串的树形结构单词共享前缀从而压缩存储并加速查找深度优先搜索DFS当遇到.通配符时需要遍历当前节点的所有子节点去递归试探递归DFS 的实现依赖递归调用逐个字符推进并在通配符处对所有可能的分支进行探索。这三种能力正好对应仓库题解 articles/design-word-search-data-structure.md 开头列出的 Prerequisites。方案一暴力解法Brute Force思路最简单的做法是原样存储所有单词每次搜索时遍历整个列表若两个字符串长度不同直接跳过逐字符比较普通字符要求完全相等而搜索串中的.视为通配符可匹配任意字符。这种做法的正确性没有问题——题目数据规模较小时它完全可行但不适合大数据集每插入一个词只是追加到列表而每次搜索都要把全部已存单词扫一遍。算法流程数据结构维护一个列表store保存所有已添加的单词。addWord(word)将word追加到列表末尾。search(word)遍历列表中每个已存单词w若len(w) ! len(word)跳过长度不同必然不匹配逐下标比较字符两字符相等或搜索字符是.继续否则立刻中断检查下一个单词若某个单词全部字符匹配成功返回true全部检查完仍未命中返回false。以 Python 为例与仓库题解一致class WordDictionary: def __init__(self): self.store [] def addWord(self, word: str) - None: self.store.append(word) def search(self, word: str) - bool: for w in self.store: if len(w) ! len(word): continue i 0 while i len(w): if w[i] word[i] or word[i] .: i 1 else: break if i len(w): return True return False复杂度分析时间addWord()为 O(1)search()为 O(m × n)其中m为已存单词数量n为字符串长度空间O(m × n)。这正是提示 1Hint 1指出的场景暴力解每次搜索都要线性扫描整个列表属于O(m * n)的解法。当单词数量m很大时性能会急剧恶化因此需要思考是否存在树形结构来替代线性列表——这就引出了 Trie。方案二Trie DFS推荐解法思路Trie 天然适合「字典 前缀共享」的场景。addWord走标准的前缀树插入流程而search遇到普通字符时按 Trie 逐层下行遇到.时则枚举当前节点的全部子节点并递归探索——只要有一条路径能走到单词结尾就返回true。仓库提示Hint 2 / Hint 3对这一点说得非常明确遇到.时不再直接匹配而是考虑该位置所有可能的字符对每个可能性递归检查剩余部分即便对.尝试所有路径由于本题模式串中最多出现两个.总复杂度仍可维持在O((26^2) * n)量级。节点定义每个 Trie 节点包含「子节点指针」与「是否为单词结尾」两个核心字段class TrieNode: def __init__(self): self.children {} self.word False不同语言在子节点的组织方式上有差异但语义一致定长数组如 Java / C / Go 用children[26]以下标c - a定位字符见 cpp/0211-design-add-and-search-words-data-structure.cpp 中TrieNode* children[26]与curr word[i] - a的写法哈希表/字典如 Python 的children {}、Go 的map[byte]*TrieNode、Swift 的[Character: TrieNode]见 go/0211-design-add-and-search-words-data-structure.go 中children map[byte]*TrieNode与make(map[byte]*TrieNode)的初始化。addWord标准 Trie 插入def addWord(self, word: str) - None: cur self.root for c in word: if c not in cur.children: cur.children[c] TrieNode() cur cur.children[c] cur.word True插入过程完全复用前缀从根出发逐个字符向下走没有的子节点就新建最后在结尾节点打上word True标记。Go 实现 go/0211-design-add-and-search-words-data-structure.go 与之完全对应if _, ok : cur.children[word[c]]; !ok检查不存在则新建结束后cur.word true。search带通配符的 DFSdef search(self, word: str) - bool: def dfs(j, root): cur root for i in range(j, len(word)): c word[i] if c .: for child in cur.children.values(): if dfs(i 1, child): return True return False else: if c not in cur.children: return False cur cur.children[c] return cur.word return dfs(0, self.root)递归逻辑拆解以下标j为起点从传入节点继续遍历剩余字符普通字符查当前节点是否存在对应子节点不存在立即返回false存在则下移通配符.遍历当前节点所有子节点对每个子节点递归调用dfs(i 1, child)任一分支成功即返回true全部失败才返回false模式串耗尽此时已走到某个节点必须返回cur.word——只有「完整单词的结尾」才算命中。同样地Go 版本 go/0211-design-add-and-search-words-data-structure.go 与 C 版本 cpp/0211-design-add-and-search-words-data-structure.cpp 都采用完全一致的 DFS 结构C 的searchInNode在word[i] ! .时走单一路径否则for (int j 0; j 26; j)遍历全部 26 个子节点递归试探。复杂度分析时间addWord()为 O(n)n 为单词长度search()为 O(n)通配符分支上限为 26² 常数整体仍线性空间O(t n)其中t为 Trie 中创建的节点总数n为递归栈深度。这正是提示文档开头给出的 Recommended Time Space Complexity。与暴力解相比Trie 方案把搜索从「扫描全部单词」降为「沿着共享前缀下行」本质是用插入时的前缀共享换取查询时的路径复用。常见陷阱Common Pitfalls仓库题解专门总结了三个高频错误这里逐一展开陷阱一把「前缀存在」当成「单词存在」模式串走完后必须检查当前节点是否为单词结尾而不是节点存在就返回true。例如只添加了apple搜索app时 Trie 中确实存在app路径上的节点但它并不是完整单词应当返回false# 错误任何前缀匹配都返回 True def dfs(j, root): # ... 遍历逻辑 ... return True # 错误添加 apple 后 app 也会匹配 # 正确检查当前节点是否为单词结尾 def dfs(j, root): # ... 遍历逻辑 ... return cur.word # 只有完整单词在此结束时才为 True陷阱二通配符只尝试了第一个子节点遇到.时必须尝试所有子节点。常见的错误写法是只取第一个子节点递归导致漏掉真正匹配的分支# 错误只检查第一个子节点 if c .: if cur.children and dfs(i 1, list(cur.children.values())[0]): return True # 正确必须尝试全部子节点 if c .: for child in cur.children.values(): if dfs(i 1, child): return True return False陷阱三缺失路径的返回时机普通字符找不到对应子节点时立即返回false而通配符分支必须在遍历完所有可能性之后才返回false两者返回时机不同混用会导致结果错误if c .: for child in cur.children.values(): if child is not None and dfs(i 1, child): return True return False # 所有路径都失败才返回 False else: if c not in cur.children: return False # 该字符路径不存在 cur cur.children[c]仓库中的多语言实现对照本仓库对该题提供了覆盖 12 语言的可运行实现均可直接对照阅读语言实现文件节点结构特点Pythonpython/0211-design-add-and-search-words-data-structure.pychildren字典 word布尔标记Gogo/0211-design-add-and-search-words-data-structure.gomap[byte]*TrieNodeDFS 用闭包递归Ccpp/0211-design-add-and-search-words-data-structure.cppchildren[26]定长数组searchInNode递归Rubyruby/0211-design-add-and-search-words-data-structure.rb直接以嵌套 Hash 作 TrieEND键标记结尾Java / JavaScript / C# / Kotlin / Swift / Rust / Scala / TypeScript / C同名0211-*文件各自语言惯用的数组或字典实现其中 Ruby 实现 ruby/0211-design-add-and-search-words-data-structure.rb 是一个有趣的变体它不定义显式的TrieNode类而是直接用嵌套 Hash 充当 Trie并用特殊键END标记单词结尾搜索时对.分支用curr.keys.any?对所有子键递归。这证明了同一思路在不同语言抽象下的多种落地方式阅读时值得对照体会。另外该数据结构也常与「二维网格单词搜索」联动出现——本仓库的 java/0212-word-search-ii.java、python/0212-word-search-ii.py 等 LeetCode 212 实现正是「Trie 网格 DFS」的进阶应用可作为延伸学习。总结针对「支持.通配符的单词字典」暴力解正确但低效addWordO(1)、searchO(m × n)仅适合单词量很小的场景Trie DFS是标准最优解插入沿用前缀树流程搜索对普通字符线性下行、对.枚举全部子节点递归试探达到 O(n) 查询与 O(t n) 空间三个关键易错点——结尾标记判定、通配符全分支枚举、缺失路径的返回时机——务必在实现时逐一核对。该解法不仅是 LeetCode 211 的标准答案其「字典 通配符递归搜索」的骨架还可直接迁移到拼写检查、自动补全、正则模糊匹配、网格单词搜索LeetCode 212等真实场景具有很高的复用价值。【免费下载链接】leetcodeLeetcode solutions项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表