LeetCode 211. 添加与搜索单词 - 数据结构设计:字典树+DFS解法详解

0 阅读7分钟

在LeetCode的字符串类题目中,“添加与搜索单词”是一道经典的设计题,核心考察**字典树(Trie)**的应用,同时结合深度优先搜索(DFS)解决通配符匹配问题。这道题不仅能帮我们掌握字典树的基本操作,还能理解递归在复杂匹配场景中的灵活运用,今天就来一步步拆解这道题的解题思路和代码实现。

一、题目解读:核心需求与难点

题目要求我们设计一个WordDictionary类,支持两个核心操作:

  1. addWord(word):将单词添加到数据结构中,用于后续匹配;

  2. search(word):判断数据结构中是否存在与目标单词匹配的字符串,其中word中可能包含.,每个.可以匹配任意一个英文字母。

这道题的难点不在于“添加单词”,而在于“搜索单词”中的通配符.——它打破了常规的精确匹配,需要我们在搜索时考虑所有可能的字母组合,这也是为什么需要结合DFS的原因。

二、数据结构选择:为什么是字典树?

面对“字符串添加”和“前缀/模糊匹配”的场景,字典树(Trie)是最优选择之一,原因如下:

  • 字典树的结构天然适合存储字符串集合,每个节点代表一个字符,从根节点到叶子节点的路径对应一个完整单词,添加单词时效率为O(n)(n为单词长度);

  • 对于模糊匹配(如包含.),字典树可以方便地遍历当前节点的所有子节点,结合递归(DFS)实现通配符的匹配逻辑;

  • 相比哈希表,字典树在处理前缀匹配、模糊匹配时,空间利用率更高,且避免了哈希冲突的问题。

字典树的每个节点需要包含两个核心属性:

  1. children:存储当前节点的子节点,key为字符,value为子节点对象(形成递归结构);

  2. isEnd:标记当前节点是否为某个单词的结尾(避免区分“前缀”和“完整单词”)。

三、完整代码实现(TypeScript)

先给出完整的代码实现,后续逐方法拆解核心逻辑,代码兼顾可读性和效率,与题目要求完全匹配:

class WordDictionary {
  private children: Record<string, WordDictionary>;
  private isEnd: boolean;
  constructor() {
    this.children = {}; // 存储子节点,key为字符,value为子节点
    this.isEnd = false; // 标记当前节点是否是单词结尾
  }

  addWord(word: string): void {
    let node: WordDictionary = this; // 从根节点开始遍历
    for (const ch of word) {
      // 如果当前字符对应的子节点不存在,创建新节点
      if (!node.children[ch]) {
        node.children[ch] = new WordDictionary();
      }
      // 移动到子节点,继续处理下一个字符
      node = node.children[ch];
    }
    // 单词遍历结束,标记当前节点为单词结尾
    node.isEnd = true;
  }

  search(word: string): boolean {
    // 深度优先搜索:index表示当前匹配到的单词下标,node表示当前遍历的字典树节点
    const dfs = (index: number, node: WordDictionary): boolean => {
      // 递归终止条件:匹配完所有字符,判断当前节点是否是单词结尾
      if (index === word.length) {
        return node.isEnd;
      }
      const ch = word[index];
      // 情况1:当前字符不是通配符,精确匹配
      if (ch !== '.') {
        const child = node.children[ch];
        // 子节点存在,且递归匹配下一个字符成功,返回true
        if (child && dfs(index + 1, child)) {
          return true;
        }
      } else {
        // 情况2:当前字符是通配符,遍历所有子节点,尝试匹配
        for (const str in node.children) {
          const child = node.children[str];
          // 任意一个子节点递归匹配成功,就返回true
          if (child && dfs(index + 1, child)) {
            return true;
          }
        }
      }
      // 以上情况都不满足,匹配失败
      return false;
    }
    // 从根节点、下标0开始DFS
    return dfs(0, this);
  }
}

/**
 * Your WordDictionary object will be instantiated and called as such:
 * var obj = new WordDictionary()
 * obj.addWord(word)
 * var param_2 = obj.search(word)
 */

四、核心方法拆解

1. 构造函数(constructor)

初始化字典树的根节点:

  • children:用对象(Record)存储子节点,键是字符(如'a'、'b'),值是子节点实例,这样可以快速通过字符定位子节点;

  • isEnd:初始化为false,因为根节点本身不代表任何单词的结尾。

2. 添加单词(addWord方法)

添加单词的逻辑本质是“构建字典树”,步骤如下:

  1. 从根节点(this)开始,遍历单词的每一个字符;

  2. 对于当前字符,如果当前节点的children中没有该字符对应的子节点,就创建一个新的WordDictionary实例作为子节点;

  3. 将当前节点移动到该子节点,继续处理下一个字符;

  4. 当单词遍历完毕,将最后一个节点的isEnd设为true,标记这是一个完整单词的结尾。

示例:添加单词"apple",会从根节点开始,依次创建'a'、'p'、'p'、'l'、'e'对应的节点,最后将'e'节点的isEnd设为true

3. 搜索单词(search方法)

搜索单词是本题的核心,由于存在通配符.,我们采用**深度优先搜索(DFS)**来处理所有可能的匹配情况,核心逻辑分为两部分:

(1)DFS辅助函数

定义一个内部DFS函数,接收两个参数:

  • index:当前匹配到单词的第几个字符(从0开始);

  • node:当前遍历到字典树的哪个节点。

递归终止条件:当index等于单词长度时,说明已经匹配完所有字符,此时只需判断当前节点的isEnd是否为true(即是否是一个完整单词)。

(2)两种匹配情况

  1. 精确匹配(当前字符不是'.')

    • 检查当前节点的children中是否存在该字符对应的子节点;

    • 如果存在,递归调用DFS,处理下一个字符(index+1),并将当前子节点作为新的node

    • 如果子节点不存在,或递归匹配失败,继续判断其他情况(最终返回false)。

  2. 通配符匹配(当前字符是'.')

    • 通配符可以匹配任意字符,因此需要遍历当前节点的所有子节点;

    • 对每个子节点,递归调用DFS,处理下一个字符;

    • 只要有一个子节点递归匹配成功,就直接返回true(剪枝优化,避免不必要的遍历)。

示例:搜索"ap.ple",当遍历到第3个字符'.'时,会遍历当前节点(第二个'p')的所有子节点,只要有一个子节点(此处是'l')能继续匹配后续字符,就返回true

五、易错点与优化思路

1. 易错点总结

  • 忘记标记isEnd:添加单词时,如果不将最后一个节点的isEnd设为true,会导致搜索时无法区分“前缀”和“完整单词”(例如添加"app"后,搜索"app"会返回false);

  • DFS递归终止条件错误:终止条件必须是index === word.length,而不是node.children为空,否则会漏掉“单词长度与路径长度一致但不是结尾”的情况;

  • 通配符遍历遗漏:遍历子节点时,要确保遍历所有children中的键,不能遗漏任何一种可能的字符。

2. 优化思路

  • 空间优化:如果单词集合中重复前缀较少,字典树的空间优势不明显,可以考虑结合哈希表分组(按单词长度分组),搜索时先根据长度过滤,再进行匹配,减少DFS的次数;

  • 时间优化:在DFS中加入剪枝逻辑,一旦找到匹配的子节点,立即返回true,避免不必要的遍历;

  • 边界处理:针对空字符串、单个字符、全通配符(如"....")等边界情况,提前做判断,提升效率。

六、总结

LeetCode 211题的核心是“字典树+DFS”的组合应用:字典树负责高效存储和快速定位字符串前缀,DFS负责处理通配符带来的模糊匹配问题。通过这道题,我们不仅掌握了字典树的基本操作(添加、遍历),还理解了递归在复杂匹配场景中的应用逻辑。

其实字典树的应用非常广泛,比如搜索引擎的自动补全、拼写检查、IP路由最长前缀匹配等场景,掌握这道题的思路,能为后续解决类似的字符串匹配问题打下坚实的基础。