通配符 '.' 是什么
'.' 匹配「恰好一个」任意小写字母——不是跳过,也不是任意多个。
".ad" 长度仍是 3,匹配 bad/dad/mad/pad。
给词典加上通配搜索:'.' 匹配任意一个字符,让「按字符走一条路」变成「在岔路口对每个孩子分别试」。
只支持精确查询用 HashSet 就够;难点是 search 里的 . —— 它匹配任意单字符。用 Trie 存词,普通字符沿唯一边走,遇到 . 就对所有非空孩子 DFS 扇出。下面两幕分别讲「直觉」和「机制」。
设计一个支持通配搜索的词典,要实现两个能力:
只支持精确查询的话,HashSet 就够了。问题出在那个 .——它能匹配任意单个字符,你没法拿一个含通配的串去查字面 key。
路径 b→a 存在,但 ba 节点没有 isEnd 印章——它只是 bad 的前缀,不是被加入过的完整单词。模式读完还得落点 isEnd=true 才算命中。
'.' 匹配「恰好一个」任意小写字母——不是跳过,也不是任意多个。
".ad" 长度仍是 3,匹配 bad/dad/mad/pad。
精确查询用 HashSet 就够;但带 '.' 时你不知道要查哪个 key。
".ad" 不等于任何字面 key,HashSet 直接漏掉。
把单词拆成「从 root 逐字符的路径」,相同前缀共享一段路。
bad/dad/mad/pad 在 root 下分成 4 条 →a→d 链。
遇到 '.' 就对每个孩子递归深入,失败就回溯换下一个孩子。
只要有一条成功就立刻返回 true(短路)。
模式读完只说明路径存在,还要落点 isEnd=true 才是完整单词。
"ba" 能走到,但 ba 无 isEnd → false。
addWord 存进集合,search 直接 set.has。精确查询 O(1),但 '.' 一来就废——你没法用一个含通配的串去查字面 key。
把单词按长度分桶,search 时拿同长度的逐个字符比对('.' 处跳过比较)。能work,但最坏要扫该长度下的所有单词,没利用「共享前缀」。
用 Trie 存单词。普通字符沿唯一边走,天然共享前缀;这正是 LC208 的结构,addWord 与 insert 完全一样。
唯一新增:search 遇 '.' 不再是单指针,而是对当前节点所有非空孩子递归深入,任一成功即 true。这就是本题的全部增量。
跳过 → 长度会少 1,".ad" 变成匹配 2 字符;正则 '*' → 匹配任意多个字符。这两种都不对。
它消费模式里一格、也消费树里一条边,只是不在乎走的是哪条边——于是从单指针裂成「对每个孩子各试一次」。
词典里已存入 bad / dad / mad / pad。现在 search(".ad"),从 root 出发,普通字符只能走对应那条边,'.' 则要对所有存在的孩子分别试。
这一步:调用 search(".ad"),转交 dfs(root, 0)。
为什么:搜索从 root 开始,逐字符消费模式串。
1type Node struct {2 children [26]*Node3 isEnd bool4}56func (d *WordDictionary) AddWord(word string) {7 node := d.root8 for _, c := range word {9 i := c - 'a'10 if node.children[i] == nil {11 node.children[i] = &Node{}12 }13 node = node.children[i]14 }15 node.isEnd = true16}1718func (d *WordDictionary) Search(word string) bool {19 return dfs(d.root, word, 0)20}2122func dfs(node *Node, word string, i int) bool {23 if i == len(word) {24 return node.isEnd25 }26 c := word[i]27 if c == '.' {28 for _, child := range node.children {29 if child != nil && dfs(child, word, i+1) {30 return true31 }32 }33 return false34 }35 next := node.children[c-'a']36 if next == nil {37 return false38 }39 return dfs(next, word, i+1)40}
逐字符插入,最多新建 L 个节点。
退化成一条路径下行,和 LC208 查询同级;空间是递归栈深。
每个 '.' 最坏分裂 26 路,叠加成指数级搜索树;上界也被节点总数 Σ 限制。
普通字符是单路 walk;'.' 是分叉搜索。通配越多、越靠前,扇出越猛,这正是动画要让你看见的代价。
用 Trie 存所有单词,addWord 和实现 Trie 的 insert 一样、终点置 isEnd。search 写成 dfs:普通字符沿唯一孩子走;遇到 '.' 就对当前节点所有非空孩子递归,任一成功即返回 true。命中条件是某条路径读完模式且落点 isEnd。复杂度:addWord 与无通配的 search 都是 O(L);含 k 个 '.' 时最坏 O(26^k·L),因为每个通配位都可能分裂成多路 DFS。
'.' 消费「恰好一个」字符,长度不变。跳过会让 ".ad" 去匹配长度 2 的词,错。
正则 '*' 是「任意多个」,'.' 只是「任意一个」。本题没有「零个或多个」的语义。
应只对非空孩子递归。凭空枚举 'a'..'z' 会去访问大量 nil,既慢又易空指针。
任一孩子返回 true 就该立刻 return true 短路;继续遍历是白做无用功。
模式读完只代表路径存在。漏掉 node.isEnd 会把 "ba" 这种纯前缀误判成命中。