79. 单词搜索
问题描述
给定一个 m x n
的字符网格 board
和一个字符串 word
,判断 word
是否存在于网格中。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格指水平或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
解释:路径 A → B → C → C → E → D 可以构成单词
算法思路
回溯法(DFS)
:
- 遍历起点:
遍历网格的每个位置作为搜索起点。 - DFS 搜索:
- 若当前字符匹配,向
四个方向(上、右、下、左)
递归搜索下一字符 - 使用原地修改标记已访问(避免额外空间)
- 搜索后恢复网格状态(回溯)
- 若当前字符匹配,向
- 终止条件:
- 成功:匹配完整个单词
- 失败:越界或字符不匹配
代码实现
class Solution {
// 四个移动方向:上、右、下、左
private static final int[][] DIRECTIONS = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
public boolean exist(char[][] board, String word) {
int m = board.length, n = board[0].length;
// 遍历每个起点
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (dfs(board, word, i, j, 0)) {
return true;
}
}
}
return false;
}
/**
* 回溯搜索函数
*
* @param board 字符网格
* @param word 目标单词
* @param i 当前行坐标
* @param j 当前列坐标
* @param index 当前匹配的字符索引
* @return 是否匹配成功
*/
private boolean dfs(char[][] board, String word, int i, int j, int index) {
// 终止条件1:越界或字符不匹配
if (i < 0 || i >= board.length || j < 0 || j >= board[0].length
|| board[i][j] != word.charAt(index)) {
return false;
}
// 终止条件2:已匹配整个单词
if (index == word.length() - 1) {
return true;
}
// 标记当前单元格已访问(原地修改)
char temp = board[i][j];
board[i][j] = '#'; // 使用特殊字符标记
// 向四个方向递归搜索
for (int[] dir : DIRECTIONS) {
int newI = i + dir[0], newJ = j + dir[1];
if (dfs(board, word, newI, newJ, index + 1)) {
return true;
}
}
// 回溯:恢复网格状态
board[i][j] = temp;
return false;
}
}
代码注释
代码部分 | 说明 |
---|---|
DIRECTIONS | 定义四个移动方向(上、右、下、左) |
exist() 双层循环 | 遍历所有可能的搜索起点 |
dfs() 边界检查 | 检查坐标是否越界或字符不匹配 |
index == word.length()-1 | 成功匹配整个单词 |
board[i][j] = '#' | 原地修改标记已访问 |
for (int[] dir : DIRECTIONS) | 遍历四个方向递归搜索 |
board[i][j] = temp | 回溯时恢复网格状态 |
算法过程
board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]]
, word = "ABCCED"
:
-
起点 (0,0):
- 匹配 ‘A’ → 标记为 ‘#’
- 向右 (0,1):匹配 ‘B’ → 标记
- 向右 (0,2):匹配 ‘C’ → 标记
- 向下 (1,2):匹配 ‘C’ → 标记
- 向下 (2,2):匹配 ‘E’ → 标记
- 向左 (2,1):匹配 ‘D’ → 成功(
index=5
)
-
搜索路径:
(0,0) → (0,1) → (0,2) → (1,2) → (2,2) → (2,1)
复杂度分析
- 时间复杂度:O(mn × 3^L)
- 最坏情况:遍历所有起点 (m×n),每个起点 DFS 搜索约 3^L 次(L 为单词长度)
- 每次递归有 3 个方向可选(因不会回退)
- 空间复杂度:O(L)
- 递归调用栈深度最大为 L(单词长度)
关键点
回溯框架
:- 选择:匹配当前字符
- 递归:向四个方向搜索下一字符
- 撤销:恢复网格状态
原地标记
:
通过修改网格为特殊字符标记访问状态,避免额外空间方向处理
:
使用方向数组简化代码,避免重复逻辑剪枝优化
:
一旦找到有效路径立即返回,不再继续搜索
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 示例测试
char[][] board1 = {
{'A','B','C','E'},
{'S','F','C','S'},
{'A','D','E','E'}
};
System.out.println(solution.exist(board1, "ABCCED")); // true
System.out.println(solution.exist(board1, "SEE")); // true
System.out.println(solution.exist(board1, "ABCB")); // false
// 单字符测试
char[][] board2 = {{'A'}};
System.out.println(solution.exist(board2, "A")); // true
// 重复字符测试
char[][] board3 = {
{'A','A','A','A'},
{'A','A','A','A'},
{'A','A','A','A'}
};
System.out.println(solution.exist(board3, "AAAAAAAA")); // true
// 无解测试
char[][] board4 = {
{'X','Y','Z'},
{'Z','Y','X'}
};
System.out.println(solution.exist(board4, "XYZZ")); // false
}
常见问题
-
为什么需要回溯恢复网格状态?
因为不同路径搜索需要独立的访问状态,恢复状态确保后续搜索的正确性。 -
为什么是 3 个方向而不是 4 个?
从当前单元格出发时,不会回退到上一个单元格(已标记访问),每个节点最多有 3 个新方向。 -
如何处理空单词?
题目保证word
非空,无需特殊处理。 -
最坏情况性能如何?
最坏情况需遍历所有路径(如全 ‘A’ 网格中搜索 “AAA…”),但实际通过起点剪枝可提前终止。