Hot 100 --- 单词搜索
本文概览:本文以LeetCode题目"单词搜索"为例,讲解为什么用DFS不用BFS,以及DFS在二维网格中的编写套路
一、题目

二、题目分析
这题要求在二维网格中找单词,有三个要求:
- 必须从单词的第一个字符开始
- 字符必须上下左右相邻(不能跳格子)
- 同一个位置的字符不能重复使用
比如要找 "ABC",网格是:
1 | A C B |
这不算,因为 A 的上下左右没有 B(A 的右边是 C,下边是 D),所以无法组成连续的 "AB"。
什么是"连续"? 就是每一步只能走到上下左右相邻的格子,不能跳着走。
思路概览
Java 实现代码如下
1 | class Solution { |
思路简要说明:
- 外层双重循环遍历每个格子作为起点,调用 DFS 尝试匹配
- DFS 内部先判断出口(匹配完成、越界、字符不匹配),再标记当前格子,向四方向递归,最后回溯
- 用一个
visited数组记录已访问的格子,回溯时恢复为false
三、思路详解
第一步:为什么用 DFS 不用 BFS
这题和前面的岛屿数量很像,都是二维网格 + 四方向递归。岛屿数量用 DFS 或 BFS 都行,但这题强烈推荐 DFS,不推荐 BFS。
为什么?因为 BFS 需要记录每条路径的访问状态。
举个例子,假设从 A 出发,走到 B 后有两条路:
1 | 路径1:A → B → C → ... |
如果用 BFS,队列里会同时存在这两条路径。路径1 访问了 C,路径2 访问了 D,它们的 visited 状态是不同的——路径1 不能再用 C,路径2 不能再用 D,但两条路径都不能用 A 和 B。
如果用一个全局 visited,路径1 标记了 C,路径2 就看不到 C 了,但路径2 可能本来是可以走 C 的(只是路径1 先走了)。
所以 BFS 要么给每个队列元素配一个独立的 visited 副本(空间开销大),要么用更复杂的状态记录方式(代码复杂)。
而 DFS 就简单多了:一个全局 visited 数组,走到哪标记到哪,走不通就回溯恢复。同一时刻只有一条路径在走,visited 状态天然就是当前路径的访问记录。
第二步:DFS 编写套路(和岛屿数量对比)
这题的 DFS 框架和岛屿数量几乎一样:
1 | // 岛屿数量的 DFS |
区别在于:
| 岛屿数量 | 单词搜索 | |
|---|---|---|
| 目标 | 标记整个岛屿 | 找到一条匹配路径 |
| 标记方式 | 直接改 grid[i][j] = '0' |
用 visited 数组 |
| 回溯 | 不需要(标记完就不管了) | 需要(走不通要恢复) |
| 返回值 | void |
boolean |
第三步:递归出口的三个判断
DFS 函数开头有三个判断,顺序很重要:
1 | // 1. 所有字符匹配完毕,直接成功 |
为什么 index == word.length() 要放在最前面?
因为当 index 到达 word.length() 时,说明所有字符都匹配完了,此时 i, j 可能是越界的(最后一个字符的下一个位置)。如果先判断越界,就会错误地返回 false。
举个例子,单词 "AB",网格是:
1 | A B |
匹配流程:
dfs(0, 0):board[0][0] = 'A',匹配,标记,递归dfs(0, 1)dfs(0, 1):board[0][1] = 'B',匹配,标记,递归dfs(0, 2)dfs(0, 2):index = 2 = word.length(),返回true
如果先判断越界,dfs(0, 2) 会因为 j >= n 返回 false,就错了。
第四步:标记 + 四方向递归 + 回溯
匹配成功后,标记当前格子,向四方向递归:
1 | visited[i][j] = true; // 标记 |
为什么要回溯?
因为当前路径走不通,要退回去尝试其他路径。如果不恢复 visited[i][j] = false,其他路径就看不到这个格子了。
举个例子,从 A 出发,先往右走 B,再往右走 C,发现 C 的下一个字符不匹配,回溯。此时 A 还要尝试往下走,如果 B 没有被恢复,A 往下走的路径就看不到 B 了(虽然这条路径本来也不经过 B,但 visited 状态是全局的)。
第五步:完整执行过程
以示例为例:
1 | board = [ |
外层循环从 (0, 0) 开始,board[0][0] = 'A',匹配 word[0],开始 DFS。
最终找到的路径在网格上长这样:
1 | 0 1 2 3 |
路径:(0,0)→(0,1)→(0,2)→(1,2)→(2,2)→(2,1),对应字符 "ABCCED"。
下面用表格展示每一步的状态变化:
| 步骤 | 当前位置 | 字符 | 匹配 word 的哪个字符 | 下一步尝试 | 结果 |
|---|---|---|---|---|---|
| 1 | (0,0) | A | word[0] | 向右到 (0,1) | 继续 |
| 2 | (0,1) | B | word[1] | 向右到 (0,2) | 继续 |
| 3 | (0,2) | C | word[2] | 向右到 (0,3) | E≠C,失败,回溯 |
| 4 | (0,2) | C | word[2] | 向下到 (1,2) | 继续 |
| 5 | (1,2) | C | word[3] | 向右到 (1,3) | S≠E,失败,回溯 |
| 6 | (1,2) | C | word[3] | 向下到 (2,2) | 继续 |
| 7 | (2,2) | E | word[4] | 向右到 (2,3) | E≠D,失败,回溯 |
| 8 | (2,2) | E | word[4] | 向左到 (2,1) | 继续 |
| 9 | (2,1) | D | word[5] | index=6=word.length() | 成功! |
注意第 3 步和第 4 步:在 (0,2) 这个位置,先尝试向右走,发现不匹配,回溯后再尝试向下走。这就是 DFS 的回溯机制——一条路走不通就退回来换一条路。
第六步:DFS vs BFS 对比
| DFS | BFS | |
|---|---|---|
| 数据结构 | 递归栈 | 队列 |
| visited | 一个全局数组 | 每个路径需要独立副本 |
| 回溯 | 天然支持(递归返回时恢复) | 需要额外处理 |
| 空间复杂度 | O(mn + L),L 是单词长度 | O(mn × 路径数) |
| 代码复杂度 | 简单 | 复杂 |
这题用 DFS 是标准做法,BFS 虽然理论上可行,但实现起来麻烦且效率低。
四、总结
这题和岛屿数量一样,都是二维网格 + 四方向递归,核心区别在于:
- 岛屿数量不需要回溯(标记完就不管了),单词搜索需要回溯(走不通要恢复)
- 岛屿数量用 DFS 或 BFS 都行,单词搜索强烈推荐 DFS(BFS 的 visited 管理太复杂)
DFS 的编写套路:
- 判断出口(匹配完成、越界、字符不匹配)
- 标记当前格子
- 四方向递归
- 回溯恢复
这个套路适用于所有"在网格中找路径"的题目。

