79. 单词搜索

0 阅读4分钟

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例 1:

输入: board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "ABCCED"
输出: true

示例 2:

输入: board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "SEE"
输出: true

示例 3:

输入: board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "ABCB"
输出: false

提示:

  • m == board.length
  • n = board[i].length
  • 1 <= m, n <= 6
  • 1 <= word.length <= 15
  • board 和 word 仅由大小写英文字母组成

1. 生活案例:特工在迷宫中搜寻暗号

想象你是一名特工,被派往一个 m×nm \times n 的房间矩阵里寻找一份秘密文件(单词 word)。

  • 规则

    1. 你必须按照暗号字母的顺序走(先找第一个字母,再找相邻的第二个...)。
    2. 你只能向上、下、左、右相邻的房间移动。
    3. 关键约束:在同一次任务中,你不能重复进入同一个房间(走过的路要插个旗子标记,回来时再拔掉)。
  • 过程

    1. 你从矩阵的每一个房间开始尝试(外层双循环)。
    2. 进入房间,如果字母对上了,你就在脚下抹上油漆(标记为 '#'),然后去敲邻居的门。
    3. 如果四个邻居都带不向终点,说明这条路走错了,你得把脚下的油漆擦干净(还原字符),退回到上一个房间重新选路。

2. 代码实现与详细注释

这是你图片中的代码,我为你添加了详细的“特工行动”注释:

JavaScript

/**
 * @param {character[][]} board - 字母矩阵
 * @param {string} word - 目标单词
 * @return {boolean}
 */
var exist = function (board, word) {
    let m = board.length;
    let n = board[0].length;

    // 核心:深度优先搜索 (DFS)
    function dfsSearch(i, j, len) {
        // 【成功出口】:如果匹配的长度等于单词长度,说明全找到了
        if (len == word.length) return true;

        // 【失败出口】:
        // 1. 越界(跑出矩阵了)
        // 2. 当前房间字母不对,或者这个房间刚才已经走过了(标记为'#'的)
        if (i < 0 || i >= m || j < 0 || j >= n || board[i][j] !== word[len]) {
            return false;
        }

        // 【执行搜寻】:
        // 1. 先把当前房间做个标记,防止这一趟任务里回头重复走
        let temp = board[i][j];
        board[i][j] = '#'; 

        // 2. 派分身去四个方向探路
        // 只要有一个分身说“找到了”,结果就是 true
        let res = dfsSearch(i - 1, j, len + 1) || // 上
                  dfsSearch(i + 1, j, len + 1) || // 下
                  dfsSearch(i, j - 1, len + 1) || // 左
                  dfsSearch(i, j + 1, len + 1);   // 右

        // 3. 【回溯关键点】:擦除油漆,拔掉旗子
        // 不管有没有找到,都要把房间还原,不能影响特工下一次从别的起点开始的搜寻
        board[i][j] = temp;

        return res;
    }

    // 从矩阵的每一个格子作为起点开始“地毯式搜索”
    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            // 如果某一个起点出发能成功,直接收工返回 true
            if (dfsSearch(i, j, 0)) return true;
        }
    }

    return false; // 找遍了所有起点都无果
};

3. 核心原理解析

为什么必须回溯 board[i][j] = temp

回溯是算法的“后悔药”。

假设你要找 "ABCCED",你在路径 A -> B -> C 走到了死胡同。如果你不把路径上的字母还原,当你从另一个起点(比如旁边的另一个 A)再次搜索路过这些格子时,它们会被误认为“已访问过”,从而导致漏掉正确答案。

为什么用 DFS 而不是 BFS?

  • DFS(深度优先) :像特工潜入,一条路走到黑,不行再回头。它配合回溯非常节省内存,只需要记录当前的路径。
  • BFS(广度优先) :像洪水暴发,同时向四周蔓延。在处理“是否存在某条路径”且包含“不能重复访问”逻辑时,BFS 会产生海量的状态记录,效率较低。

复杂度分析

  • 时间复杂度O(MN3L)O(M \cdot N \cdot 3^L)。其中 M,NM, N 是矩阵大小,LL 是单词长度。每个格子都要尝试,每个字母有 3 个方向可走(除了来的那个方向)。
  • 空间复杂度O(L)O(L)。递归的最大深度就是单词的长度。