LeetCode 79. 单词搜索:DFS回溯解法详解

在LeetCode的中等难度题目中,「单词搜索」是一道经典的DFS(深度优先搜索)+ 回溯算法应用题。它不仅考察对搜索算法的理解,更考验对边界条件的把控和空间优化的思路。今天就带大家一步步拆解这道题,从题目分析到代码实现,再到易错点总结,帮你彻底搞懂这道高频面试题。

一、题目解读

先明确题目要求,避免理解偏差:

  • 给定一个 m x n 的二维字符网格 board 和一个字符串 word;

  • 判断 word 是否能在网格中找到,要求:字母顺序一致,通过「相邻单元格」(水平或垂直相邻,斜向不算)构成;

  • 同一个单元格的字母不允许重复使用

  • 找到则返回 true,否则返回 false。

举个简单例子:若 board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]],word = "ABCCED",则返回 true;若 word = "ABCB",则返回 false(因为第二个B和第一个B不相邻,且无法重复使用同一个B)。

二、核心思路:DFS + 回溯

这道题的核心是「深度优先搜索」,搭配「回溯」来处理"重复使用单元格"的问题。为什么选择DFS?因为我们需要从某个单元格出发,沿着相邻方向一步步匹配单词的每个字符,直到匹配完所有字符(成功),或走到死路(失败,回溯到上一步)。

核心逻辑拆解:

  1. 遍历整个网格,找到单词的「起始字符」(即 word[0]),以此作为DFS的起点;

  2. 从起点出发,向上下左右四个方向探索,判断下一个单元格的字符是否与单词的下一个字符匹配;

  3. 为了避免重复使用单元格,在探索当前单元格时,先将其标记为"已使用"(比如用特殊字符#覆盖);

  4. 若探索到某一步,字符不匹配或越界,则回溯:将当前单元格的标记恢复,回到上一步继续探索其他方向;

  5. 若匹配到单词的最后一个字符,则直接返回 true;若遍历完所有起点都无法匹配,返回 false。

三、完整代码实现(TypeScript)

下面是题目给出的完整代码,我们逐行拆解每一部分的作用,让你看懂每一行代码的意义:

typescript 复制代码
function exist(board: string[][], word: string): boolean {
  const rows = board.length; // 网格的行数
  const cols = board[0].length; // 网格的列数(假设board非空)
  // 定义四个移动方向:上、下、左、右(行和列的偏移量)
  const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]];
  const wordLen = word.length; // 单词的长度,用于判断是否匹配完成

  // DFS函数:参数为当前单元格坐标(row, col)和当前匹配到的单词索引(index)
  const dfs = (row: number, col: number, index: number): boolean => {
    // 1. 终止条件1:当前单元格字符与单词当前索引字符不匹配,直接返回false
    if (board[row][col] !== word.charAt(index)) {
      return false;
    }
    // 2. 终止条件2:已经匹配到单词的最后一个字符,返回true(匹配成功)
    else if (index === wordLen - 1) {
      return true;
    }
    // 3. 标记当前单元格为已使用(避免重复使用)
    const temp = board[row][col]; // 保存当前单元格的原始字符,用于回溯
    board[row][col] = '#'; // 用#覆盖,标记为已使用

    // 4. 遍历四个方向,进行深度优先搜索
    for (const [dx, dy] of directions) {
      const newR = row + dx; // 新的行坐标
      const newC = col + dy; // 新的列坐标
      // 过滤无效坐标:越界 或 已使用(#标记)
      if (newR < 0 || newR >= rows || newC < 0 || newC >= cols || board[newR][newC] === '#') {
        continue; // 跳过无效方向
      }
      // 递归探索下一个方向,若找到匹配则直接返回true
      if (dfs(newR, newC, index + 1)) return true;
    }

    // 5. 回溯:恢复当前单元格的原始字符(当前方向探索失败,回到上一步)
    board[row][col] = temp;
    // 6. 所有方向都探索失败,返回false
    return false;
  }

  // 遍历整个网格,寻找单词的起始字符(word[0]),作为DFS的起点
  for (let i = 0; i < rows; i++) {
    for (let j = 0; j < cols; j++) {
      if (dfs(i, j, 0)) { // 若从(i,j)出发能匹配到完整单词,返回true
        return true;
      }
    }
  }

  // 所有起点都探索完毕,仍未匹配到,返回false
  return false;
};

四、关键代码解析(重点必看)

1. 方向数组 directions

directions = [[-1, 0], [1, 0], [0, -1], [0, 1]] 表示四个移动方向:

  • -1, 0\]:向上移动(行减1,列不变);

  • 0, -1\]:向左移动(列减1,行不变);

用循环遍历这个数组,就能实现"向四个方向探索"的逻辑,避免重复写四次判断。

2. DFS函数的终止条件

这是DFS的核心,直接决定了搜索的效率和正确性:

  • 第一个终止条件:当前单元格字符 != word[index],说明当前路径无法匹配,直接返回false;

  • 第二个终止条件:index === wordLen - 1,说明已经匹配到单词的最后一个字符,此时前面的字符都已匹配成功,返回true。

3. 回溯的核心:标记与恢复

这是解决"单元格不能重复使用"的关键:

  • 在探索当前单元格前,用 temp 保存原始字符,再将其设为 #(标记为已使用);

  • 当四个方向都探索失败后,再将 temp 恢复到当前单元格(回溯),让后续的搜索可以重新使用这个单元格。

举个例子:若从A出发,探索到B后发现后续无法匹配,就会回溯,将B恢复为原始字符,再去探索A的其他方向(比如A的下方)。

4. 网格遍历的作用

外层的双重for循环,是为了遍历网格中的每一个单元格,寻找单词的起始字符(word[0])。因为单词的起点可能在网格的任意位置,所以必须逐个尝试。

五、易错点总结(避坑指南)

这道题看似简单,但很容易踩坑,分享几个常见的错误点:

  1. 忘记边界判断:新坐标 newR 和 newC 必须在 [0, rows-1] 和 [0, cols-1] 范围内,否则会出现数组越界错误;

  2. 忘记回溯恢复:探索失败后,没有将 # 恢复为原始字符,导致后续搜索无法使用该单元格,漏判正确路径;

  3. 提前返回的时机:当某一个方向的DFS返回true时,要直接返回true,避免继续遍历其他方向(浪费时间);

  4. 假设board非空:代码中直接使用 board[0].length,若board为空(rows=0),会报错。实际做题时可添加边界判断(if (rows === 0) return false;),但LeetCode测试用例中board通常非空。

六、复杂度分析

了解复杂度,能帮你更好地理解算法的效率,应对面试中的提问:

  • 时间复杂度:O(m×n×3^k),其中 m 是网格行数,n 是网格列数,k 是单词长度。

解释:每个单元格最多被访问一次,每次访问时要探索3个方向(排除来时的方向),最多探索k层(单词长度),因此总时间复杂度为 O(m×n×3^k)。

  • 空间复杂度:O(k),主要是DFS递归调用的栈空间,递归深度最多为k(单词长度);若考虑网格的标记(修改原数组),则空间复杂度为O(1)(不使用额外空间)。

七、总结与拓展

这道题的核心是「DFS + 回溯」,核心思想是"探索-标记-回溯-再探索",本质上是对所有可能的路径进行遍历,找到符合条件的路径。

拓展思考:

  • 如果网格很大、单词很长,如何优化?可以考虑剪枝(比如提前判断网格中单词的字符数量是否足够,若单词中某个字符在网格中不存在,直接返回false);

  • 如果不允许修改原网格(即不能用#标记),可以用一个额外的二维布尔数组记录是否已使用,此时空间复杂度变为 O(m×n)。

其实,LeetCode中很多回溯题(比如岛屿问题、路径搜索问题)都用到了类似的思路,掌握这道题的解法,能帮你举一反三,轻松应对同类题目。

相关推荐
眼眸流转2 小时前
LeetCode热题100(四)
算法·leetcode·职场和发展
小奶包他干奶奶2 小时前
将svg对象化,并动态修改svg图标的颜色、尺寸等
前端·html
Lee川2 小时前
React 快速入门:Vue 开发者指南
前端·vue.js·react.js
相信神话20212 小时前
第零章:新手的第一课:正确认知游戏开发
大数据·数据库·算法·2d游戏编程·godot4·2d游戏开发
汀沿河2 小时前
2 模型预训练、微调、强化学习的格式
人工智能·算法·机器学习
用户6158139695162 小时前
Elpis: 基于vue3+webpack5+nodejs搭建一个完整项目
前端
90后的晨仔2 小时前
S C:\WINDOWS\system32> pnpm i -g openclaw@latest pnpm : 无法加载文件 C:\xx\A
前端
颜酱3 小时前
最小生成树(MST)核心原理 + Kruskal & Prim 算法
javascript·后端·算法
啊哦呃咦唔鱼3 小时前
LeetCode hot100-3 无重复字符的最长子串
算法·leetcode·职场和发展