DFS + BFS(深度优先搜索 & 广度优先搜索)
这两种搜索是遍历树 和图 的最基础方法。
DFS 像一个走迷宫的人,一条路走到黑,没路了再回头;
BFS 像水波纹,从起点一层一层向外扩散。
何时用 DFS?何时用 BFS?
| 场景 | DFS | BFS |
|---|---|---|
| 遍历所有节点 / 求所有路径 | ✅ 回溯方便 | 也可以,但较麻烦 |
| 求最短路径 / 最小步数 | ❌ 需要全搜 | ✅ 第一次到达即最短 |
| 连通性问题(是否可达) | ✅ | ✅ |
| 空间消耗 | 递归栈 O(深度) | 队列 O(宽度) |
| 适用结构 | 树、图、网格 | 树、图、网格 |
PDF 安排了 5 道题:
- DFS:Number of Islands (200)、Path Sum II (113)、Surrounded Regions (130)
- BFS:Rotting Oranges (994)、Binary Tree Level Order Traversal (102)、Word Ladder (127)(后者作为高阶题)
1. DFS 核心思想与模板
思想
DFS 会沿一个分支尽可能深地探索,直到无法继续才回溯。在代码上通常用递归实现,递归函数内部会有循环或分支选择。回溯可以看作是一种带有状态恢复的 DFS。
回溯框架(也是 DFS 的通用框架)
python
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
DFS 网格遍历模板(岛屿问题常用)
对于二维网格的 DFS,通常需要遍历每个格子,遇到 '1' 就进行深度搜索,并把遍历过的格子标记为 '0'(或 visited),避免重复。
java
void dfs(char[][] grid, int i, int j) {
// 越界或不是 '1',直接返回
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] != '1')
return;
grid[i][j] = '0'; // 标记为已访问
dfs(grid, i - 1, j); // 上
dfs(grid, i + 1, j); // 下
dfs(grid, i, j - 1); // 左
dfs(grid, i, j + 1); // 右
}
2. DFS 例题
例题一:岛屿数量(200. Number of Islands, Medium)
题目:给你一个由 '1'(陆地)和 '0'(水)组成的二维网格,计算岛屿的数量。岛屿总是被水包围,并且只有水平或垂直方向连接。
思路:遍历每个格子,如果遇到未访问的陆地 '1',岛屿数量 +1,然后用 DFS 把与它相连的所有陆地都淹没(标记为 '0')。
代码:
java
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) return 0;
int count = 0;
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if (grid[i][j] == '1') {
count++;
dfs(grid, i, j);
}
}
}
return count;
}
private void dfs(char[][] grid, int i, int j) {
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] != '1')
return;
grid[i][j] = '0';
dfs(grid, i - 1, j);
dfs(grid, i + 1, j);
dfs(grid, i, j - 1);
dfs(grid, i, j + 1);
}
时空复杂度:O(M×N),遍历每个格子一次,递归栈最深可能 O(M×N)。
例题二:路径总和 II(113. Path Sum II, Medium)
题目:给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。
思路:典型的回溯 + DFS。用一个临时列表记录当前路径,到达叶子时检查总和是否等于 targetSum,若是则加入结果集。回溯时移除末尾节点。
代码:
java
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
List<List<Integer>> res = new ArrayList<>();
dfs(root, targetSum, new ArrayList<>(), res);
return res;
}
private void dfs(TreeNode node, int remaining, List<Integer> path, List<List<Integer>> res) {
if (node == null) return;
path.add(node.val); // 做选择
if (node.left == null && node.right == null && remaining == node.val) {
res.add(new ArrayList<>(path)); // 找到一个解
} else {
dfs(node.left, remaining - node.val, path, res);
dfs(node.right, remaining - node.val, path, res);
}
path.remove(path.size() - 1); // 撤销选择(回溯)
}
例题三:被围绕的区域(130. Surrounded Regions, Medium)
题目:给你一个 m x n 的矩阵 board,元素为 'X' 或 'O'。将被 'X' 围绕的 'O' 区域填充为 'X'。任何边界上的 'O' 不会被围绕,且与其相连的 'O' 也不会被围绕。
思路:反向思维。从边界上的 'O' 出发,用 DFS 把所有与边界连通的 'O' 标记为特殊字符(如 '#')。然后遍历整个矩阵,将 'O' 变为 'X'(因为没被标记说明是被围绕的),将 '#' 恢复为 'O'。
代码:
java
public void solve(char[][] board) {
if (board == null || board.length == 0) return;
int m = board.length, n = board[0].length;
// 处理第一列和最后一列
for (int i = 0; i < m; i++) {
if (board[i][0] == 'O') dfs(board, i, 0);
if (board[i][n-1] == 'O') dfs(board, i, n-1);
}
// 处理第一行和最后一行(跳过角格子,但没关系)
for (int j = 0; j < n; j++) {
if (board[0][j] == 'O') dfs(board, 0, j);
if (board[m-1][j] == 'O') dfs(board, m-1, j);
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == 'O') board[i][j] = 'X';
else if (board[i][j] == '#') board[i][j] = 'O';
}
}
}
private void dfs(char[][] board, int i, int j) {
if (i < 0 || i >= board.length || j < 0 || j >= board[0].length || board[i][j] != 'O')
return;
board[i][j] = '#';
dfs(board, i-1, j);
dfs(board, i+1, j);
dfs(board, i, j-1);
dfs(board, i, j+1);
}
3. BFS 核心思想与模板
思想
BFS 使用队列,每次把当前节点的邻接节点加入队尾,保证先访问离起点近的节点。常用来求无权图的最短路径。
层序遍历 / 基础 BFS 模板(树)
java
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
Queue<TreeNode> q = new LinkedList<>();
if (root != null) q.offer(root);
while (!q.isEmpty()) {
int size = q.size(); // 当前层的节点个数
List<Integer> level = new ArrayList<>();
for (int i = 0; i < size; i++) {
TreeNode cur = q.poll();
level.add(cur.val);
if (cur.left != null) q.offer(cur.left);
if (cur.right != null) q.offer(cur.right);
}
res.add(level);
}
return res;
}
对于图,需要额外使用 visited 数组防止重复访问;有时可以直接修改原数据(如岛屿淹没)来节省空间。
4. BFS 例题
例题一:腐烂的橘子(994. Rotting Oranges, Medium)
题目 :网格中,0 表示空单元格,1 表示新鲜橘子,2 表示腐烂的橘子。每分钟,所有与腐烂橘子相邻(上下左右)的新鲜橘子都会腐烂。返回直到没有新鲜橘子为止所经过的最小分钟数。如果不可能,返回 -1。
思路:多源 BFS。初始时将所有腐烂的橘子入队。然后 BFS 每一层向外扩散,每扩散一层,时间 +1。最终检查是否还有新鲜橘子。
代码:
java
public int orangesRotting(int[][] grid) {
int m = grid.length, n = grid[0].length;
Queue<int[]> q = new LinkedList<>();
int fresh = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 2) q.offer(new int[]{i, j});
else if (grid[i][j] == 1) fresh++;
}
}
if (fresh == 0) return 0;
int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};
int minutes = 0;
while (!q.isEmpty()) {
int size = q.size();
boolean infected = false;
for (int i = 0; i < size; i++) {
int[] cur = q.poll();
for (int[] d : dirs) {
int x = cur[0] + d[0], y = cur[1] + d[1];
if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == 1) {
grid[x][y] = 2;
q.offer(new int[]{x, y});
fresh--;
infected = true;
}
}
}
if (infected) minutes++;
}
return fresh == 0 ? minutes : -1;
}
例题二:二叉树的层序遍历(102. Binary Tree Level Order Traversal, Medium)
前面已经给出完整模板,这是 BFS 在树上的标准应用。不加赘述。
例题三:单词接龙(127. Word Ladder, Hard)
题目 :给定两个单词 beginWord 和 endWord,以及一个单词列表 wordList,每次只能改变一个字母,且每一步得到的单词必须在列表中。求从 beginWord 到 endWord 的最短转换序列的长度。如果不存在,返回 0。
思路 :这是一道经典的"无权图最短路径 "问题。单词为节点,如果两个单词只差一个字母,则它们之间有一条边。用 BFS 从 beginWord 开始,每层代表一步,当遇到 endWord 时,当前层数 +1 就是答案。
代码(标准 BFS):
java
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
Set<String> wordSet = new HashSet<>(wordList);
if (!wordSet.contains(endWord)) return 0;
Queue<String> q = new LinkedList<>();
q.offer(beginWord);
Set<String> visited = new HashSet<>();
visited.add(beginWord);
int steps = 1;
while (!q.isEmpty()) {
int size = q.size();
for (int i = 0; i < size; i++) {
String cur = q.poll();
if (cur.equals(endWord)) return steps;
for (String neighbor : getNeighbors(cur, wordSet)) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
q.offer(neighbor);
}
}
}
steps++;
}
return 0;
}
// 生成当前单词所有合法邻居(只变一个字母且在 wordSet 中)
private List<String> getNeighbors(String word, Set<String> wordSet) {
List<String> res = new ArrayList<>();
char[] chars = word.toCharArray();
for (int i = 0; i < chars.length; i++) {
char old = chars[i];
for (char c = 'a'; c <= 'z'; c++) {
if (c == old) continue;
chars[i] = c;
String newWord = new String(chars);
if (wordSet.contains(newWord)) res.add(newWord);
}
chars[i] = old;
}
return res;
}
优化:还可以用双向 BFS 进一步提高效率。
5. BFS / DFS 选择速查
| 问题类型 | 推荐方法 | 原因 |
|---|---|---|
| 岛屿数量 | DFS / BFS 均可 | 遍历所有连通组件 |
| 路径总和 II | DFS + 回溯 | 需要记录所有路径 |
| 被围绕的区域 | DFS / BFS | 从边界开始标记连通区域 |
| 腐烂的橘子 | BFS | 求最小分钟数,层数即时间 |
| 二叉树的层序遍历 | BFS | 需要按层输出 |
| 单词接龙 | BFS | 最短转换序列,无权图最短路径 |
通用口诀:
- 要想找最短、最少步数 → BFS
- 要想找所有可能、回溯遍历 → DFS / 回溯