🌐 图的深度优先搜索(DFS)与广度优先搜索(BFS)算法详解

🎯 学习目标:掌握图的两种基本遍历算法,理解其原理、区别与实战应用

难度等级:⭐⭐☆☆☆(中等)

🕐 预计学习时长:30-45分钟


📚 目录


🎯 核心概念

🌳 什么是图遍历?

图遍历是指按照某种规则访问图中所有顶点的过程,是图算法的基础。就像探索一座城市,你需要决定:

  • 🚶‍♂️ 深度优先:先深入一条街道到底,再回头探索其他街道
  • 🗺️ 广度优先:先探索当前位置周围的所有街道,再逐步向外扩展

🎨 图的基本表示

在Java中,我们通常使用邻接表来表示图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 🎯 图的邻接表表示
class Graph {
private int V; // 顶点数量
private LinkedList<Integer>[] adj; // 邻接表

public Graph(int v) {
V = v;
adj = new LinkedList[v];
for (int i = 0; i < v; ++i) {
adj[i] = new LinkedList<>();
}
}

// 🔄 添加边(无向图)
public void addEdge(int v, int w) {
adj[v].add(w);
adj[w].add(v); // 无向图需要添加双向边
}
}

🌊 深度优先搜索(DFS)

🎯 核心思想

深度优先搜索就像走迷宫,选择一条路一直走到底,遇到死胡同就回退到上一个路口,选择另一条路继续探索。

🎨 算法流程

1
2
3
4
5
🚀 DFS遍历流程:
1. 从起始顶点开始
2. 标记当前顶点为已访问
3. 递归访问所有未访问的邻接顶点
4. 当没有未访问的邻接顶点时,回退到上一层

💻 Java实现

递归实现(最直观)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 🌊 深度优先搜索 - 递归实现
*
* @param graph 图的邻接表表示
* @param v 起始顶点
* @param visited 访问标记数组
*/
public void dfsRecursive(LinkedList<Integer>[] graph, int v, boolean[] visited) {
// ✅ 1. 标记当前顶点为已访问
visited[v] = true;
System.out.print(v + " "); // 📝 处理当前顶点

// 🔄 2. 递归访问所有未访问的邻接顶点
for (int neighbor : graph[v]) {
if (!visited[neighbor]) { // ❌ 只访问未访问的顶点
dfsRecursive(graph, neighbor, visited); // 🚀 递归深入
}
}
// ↩️ 3. 函数自然返回,相当于回退到上一层
}

迭代实现(使用栈)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 🌊 深度优先搜索 - 迭代实现(栈)
*
* @param graph 图的邻接表表示
* @param start 起始顶点
*/
public void dfsIterative(LinkedList<Integer>[] graph, int start) {
int V = graph.length;
boolean[] visited = new boolean[V];
Stack<Integer> stack = new Stack<>();

// 🚀 1. 起始顶点入栈
stack.push(start);

// 🔄 2. 当栈不为空时继续遍历
while (!stack.isEmpty()) {
// 📋 2.1 弹出栈顶元素
int v = stack.pop();

// ❌ 2.2 如果已经访问过,跳过
if (visited[v]) continue;

// ✅ 2.3 标记为已访问并处理
visited[v] = true;
System.out.print(v + " ");

// 🔄 2.4 将所有未访问的邻接顶点入栈
for (int neighbor : graph[v]) {
if (!visited[neighbor]) {
stack.push(neighbor); // 📝 注意:栈是后进先出
}
}
}
}

二维数组实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* 🌊 深度优先搜索实现(二维网格)
*/
public class DFS {
// 定义四个方向:右、下、左、上
private static final int[][] dir = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

/**
* 🌊 深度优先搜索主函数
*
* @param grid 二维网格数组
* @param visited 访问标记数组
* @param x 起始x坐标
* @param y 起始y坐标
*/
public static void dfs(char[][] grid, boolean[][] visited, int x, int y) {
// ✅ 标记当前节点为已访问
visited[x][y] = true;

// 🔄 遍历四个方向
for (int i = 0; i < 4; i++) {
int nextX = x + dir[i][0];
int nextY = y + dir[i][1];

// 🛑 检查坐标是否越界
if (nextX < 0 || nextX >= grid.length || nextY < 0 || nextY >= grid[0].length) {
continue;
}

// ❌ 跳过已访问的节点
if (!visited[nextX][nextY]) {
// 🚀 递归访问下一个节点
dfs(grid, visited, nextX, nextY);
}
}
}

/**
* 🌊 深度优先搜索 - 迭代实现(使用栈)
*
* @param grid 二维网格数组
* @param visited 访问标记数组
* @param x 起始x坐标
* @param y 起始y坐标
*/
public static void dfsIterative(char[][] grid, boolean[][] visited, int x, int y) {
// 📋 使用栈代替递归
LinkedList<int[]> stack = new LinkedList<>();
stack.push(new int[]{x, y});

while (!stack.isEmpty()) {
int[] cur = stack.pop();
int curX = cur[0];
int curY = cur[1];

// ❌ 如果已经访问过,跳过
if (visited[curX][curY]) {
continue;
}

// ✅ 标记为已访问
visited[curX][curY] = true;

// 🔄 遍历四个方向(注意:栈是后进先出,所以反向入栈)
for (int i = 3; i >= 0; i--) {
int nextX = curX + dir[i][0];
int nextY = curY + dir[i][1];

// 🛑 检查边界和访问状态
if (nextX >= 0 && nextX < grid.length && nextY >= 0 && nextY < grid[0].length && !visited[nextX][nextY]) {
stack.push(new int[]{nextX, nextY});
}
}
}
}
}

🎨 DFS遍历示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
图结构:0-1-2
|
3-4

DFS遍历过程(从顶点0开始):
0 → 1 → 2 → 3 → 4

可视化:
0(开始)

1

2

3

4

🌊 广度优先搜索(BFS)

🎯 核心思想

广度优先搜索就像水波纹一样,从起点开始,一层一层地向外扩展,先访问所有相邻的顶点,再访问相邻顶点的相邻顶点。

🎨 算法流程

1
2
3
4
5
6
7
🚀 BFS遍历流程:
1. 从起始顶点开始,加入队列
2. 当队列不为空时:
a. 取出队首顶点
b. 标记为已访问
c. 将所有未访问的邻接顶点加入队列
3. 重复步骤2直到队列为空

💻 Java实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 🌊 广度优先搜索 - 队列实现
*
* @param graph 图的邻接表表示
* @param start 起始顶点
*/
public void bfs(LinkedList<Integer>[] graph, int start) {
int V = graph.length;
boolean[] visited = new boolean[V];
Queue<Integer> queue = new LinkedList<>();

// 🚀 1. 起始顶点入队
queue.offer(start);
visited[start] = true;

// 🔄 2. 当队列不为空时继续遍历
while (!queue.isEmpty()) {
// 📋 2.1 取出队首元素
int v = queue.poll();
System.out.print(v + " "); // 📝 处理当前顶点

// 🔄 2.2 将所有未访问的邻接顶点入队
for (int neighbor : graph[v]) {
if (!visited[neighbor]) {
visited[neighbor] = true; // ✅ 标记为已访问
queue.offer(neighbor); // 📝 入队等待处理
}
}
}
}

🎨 BFS层级遍历(记录层级信息)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 🌊 广度优先搜索 - 层级遍历(记录每个顶点的层级)
*
* @param graph 图的邻接表表示
* @param start 起始顶点
* @return 每个顶点的层级(距离)
*/
public int[] bfsWithLevel(LinkedList<Integer>[] graph, int start) {
int V = graph.length;
int[] level = new int[V]; // 🎯 记录每个顶点的层级
Arrays.fill(level, -1); // 📝 初始化为-1,表示未访问
Queue<Integer> queue = new LinkedList<>();

// 🚀 起始顶点
queue.offer(start);
level[start] = 0; // 🎯 起始顶点层级为0

// 🔄 BFS遍历
while (!queue.isEmpty()) {
int v = queue.poll();
int currentLevel = level[v];

// 🔄 处理所有邻接顶点
for (int neighbor : graph[v]) {
if (level[neighbor] == -1) { // ❌ 未访问的顶点
level[neighbor] = currentLevel + 1; // 🎯 层级+1
queue.offer(neighbor);
}
}
}

return level;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import java.util.LinkedList;
import java.util.Queue;

/**
* 🌊 广度优先搜索实现(二维网格)
*/
public class BFS {
// 定义四个方向:右、下、左、上
private static final int[][] dir = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

/**
* 🌊 广度优先搜索主函数(二维网格)
*
* @param grid 二维网格数组
* @param visited 访问标记数组
* @param x 起始x坐标
* @param y 起始y坐标
*/
public static void bfs(char[][] grid, boolean[][] visited, int x, int y) {
// 定义队列,存储待访问的坐标
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{x, y}); // 起始节点加入队列
visited[x][y] = true; // 标记为已访问
// 🔍 重要特性:在网格/矩阵的BFS实现中,当第一次访问某个节点时:
// 1️⃣ 该访问路径必定是最短路径
// 2️⃣ 因为BFS是按距离从近到远逐层扩展的
// 3️⃣ 后续如果有其他路径到达该节点,距离一定不会更短(因为已经按层级遍历)

while (!queue.isEmpty()) {
int[] cur = queue.poll(); // 取出当前节点
int curX = cur[0];
int curY = cur[1];

// 遍历四个方向
for (int i = 0; i < 4; i++) {
int nextX = curX + dir[i][0];
int nextY = curY + dir[i][1];

// 检查坐标是否越界
if (nextX < 0 || nextX >= grid.length || nextY < 0 || nextY >= grid[0].length) {
continue;
}

// 如果节点未被访问过
if (!visited[nextX][nextY]) {
queue.offer(new int[]{nextX, nextY}); // 加入队列
visited[nextX][nextY] = true; // 标记为已访问
// 🔍 这里第一次访问该节点,路径一定是最短的
}
}
}
}
}

🎨 BFS遍历示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
图结构:0-1-2
|
3-4

BFS遍历过程(从顶点0开始):
层级0: 0
层级1: 1 3
层级2: 2 4

可视化:
0(层级0)
↓ ↓
1 3(层级1)
↓ ↓
2 4(层级2)

🔍 DFS vs BFS对比分析

📊 核心区别对比表

特性 DFS深度优先 BFS广度优先
数据结构 栈(Stack) 队列(Queue)
遍历顺序 先深入到底,再回退 一层一层向外扩展
空间复杂度 O(h),h是树的高度 O(w),w是树的宽度
时间复杂度 O(V+E) O(V+E)
适用场景 路径搜索、拓扑排序 最短路径、层级遍历
实现难度 简单(递归) 稍复杂(队列)
特点 可能陷入深层 保证找到最短路径

🎯 选择策略

1
2
3
4
5
6
7
8
9
10
11
🤔 什么时候用DFS?
✅ 需要找到任意路径
✅ 图的连通性问题
✅ 拓扑排序
✅ 空间受限的情况

🤔 什么时候用BFS?
✅ 需要找到最短路径
✅ 层级遍历
✅ 单词接龙类问题
✅ 社交网络中的最短距离

🏗️ 实战应用案例

🎯 案例1:岛屿数量(LeetCode 200)

问题描述:给定一个由 ‘1’(陆地)和 ‘0’(水)组成的二维网格,计算岛屿的数量。

DFS解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/**
* 🏝️ 岛屿数量解决方案(LeetCode 200)
*/
public class NumberOfIslands {
/**
* 🏝️ 计算岛屿数量(DFS解法)
*
* @param grid 二维网格
* @return 岛屿数量
*/
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) return 0;

int rows = grid.length;
int cols = grid[0].length;
int islandCount = 0;

// 🔍 遍历整个网格
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (grid[i][j] == '1') { // 🎯 发现新岛屿
dfs(grid, i, j); // 🌊 DFS淹没整个岛屿
islandCount++; // 📊 岛屿计数+1
}
}
}

return islandCount;
}

/**
* 🌊 DFS淹没岛屿(将相连的陆地变成水)
*/
private void dfs(char[][] grid, int i, int j) {
int rows = grid.length;
int cols = grid[0].length;

// 🛑 边界检查和终止条件
if (i < 0 || i >= rows || j < 0 || j >= cols || grid[i][j] == '0') {
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); // ←
}
}

**BFS解决方案**:
```java
/**
* 🏝️ 岛屿数量解决方案(LeetCode 200)
*/
public class NumberOfIslandsBFS {
/**
* 🏝️ 计算岛屿数量(BFS解法)
*
* @param grid 二维网格
* @return 岛屿数量
*/
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) return 0;

int rows = grid.length;
int cols = grid[0].length;
int islandCount = 0;

// 🔍 遍历整个网格
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (grid[i][j] == '1') { // 🎯 发现新岛屿
bfs(grid, i, j); // 🌊 BFS淹没整个岛屿
islandCount++; // 📊 岛屿计数+1
}
}
}

return islandCount;
}

/**
* 🌊 BFS淹没岛屿(将相连的陆地变成水)
*/
private void bfs(char[][] grid, int i, int j) {
int rows = grid.length;
int cols = grid[0].length;

// 📦 使用队列实现BFS
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{i, j});
grid[i][j] = '0'; // ✅ 标记当前位置为已访问(淹没)

// 🔄 四个方向的偏移量(上下左右)
int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

while (!queue.isEmpty()) {
int[] curr = queue.poll();
int x = curr[0];
int y = curr[1];

// 🔍 探索四个方向的相邻格子
for (int[] dir : directions) {
int nx = x + dir[0];
int ny = y + dir[1];

// 🛑 边界检查和有效陆地检查
if (nx >= 0 && nx < rows && ny >= 0 && ny < cols && grid[nx][ny] == '1') {
queue.offer(new int[]{nx, ny}); // 📥 将新陆地加入队列
grid[nx][ny] = '0'; // ✅ 标记为已访问
}
}
}
}
}

🎯 案例2:单词接龙(LeetCode 127)

问题描述:给定两个单词(beginWord和endWord)和一个字典,找到从beginWord到endWord的最短转换序列的长度。

BFS解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;

/**
* 📝 单词接龙解决方案(LeetCode 127)
*/
public class WordLadder {
/**
* 📝 单词接龙 - BFS解法(最短路径)
*
* @param beginWord 起始单词
* @param endWord 目标单词
* @param wordList 单词字典
* @return 最短转换序列长度
*/
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
Set<String> wordSet = new HashSet<>(wordList);

// ❌ 如果目标单词不在字典中,无法到达
if (!wordSet.contains(endWord)) return 0;

Queue<String> queue = new LinkedList<>();
Set<String> visited = new HashSet<>();

// 🚀 BFS初始化
queue.offer(beginWord);
visited.add(beginWord);
int level = 1; // 🎯 记录层级(路径长度)

while (!queue.isEmpty()) {
int size = queue.size();

// 🎯 处理当前层级的所有单词
for (int i = 0; i < size; i++) {
String currentWord = queue.poll();

// 🎯 找到目标,返回当前层级
if (currentWord.equals(endWord)) return level;

// 🔄 生成所有可能的下一个单词
for (String nextWord : getNextWords(currentWord, wordSet, visited)) {
visited.add(nextWord);
queue.offer(nextWord);
}
}

level++; // 🎯 进入下一层级
}

return 0; // ❌ 无法到达目标
}

/**
* 📝 生成所有可能的下一个单词(改变一个字母)
*/
private List<String> getNextWords(String word, Set<String> wordSet, Set<String> visited) {
List<String> result = new ArrayList<>();
char[] chars = word.toCharArray();

// 🔄 尝试改变每个位置的字母
for (int i = 0; i < chars.length; i++) {
char original = chars[i];

// 🔄 尝试所有可能的字母
for (char c = 'a'; c <= 'z'; c++) {
if (c == original) continue; // ❌ 跳过原字母

chars[i] = c;
String newWord = new String(chars);

// ✅ 如果新单词在字典中且未访问过,加入结果
if (wordSet.contains(newWord) && !visited.contains(newWord)) {
result.add(newWord);
}
}

chars[i] = original; // ↩️ 恢复原字母
}

return result;
}
}

⚡ 性能分析与优化

📊 时间复杂度对比

算法 时间复杂度 空间复杂度 适用场景
DFS O(V + E) O(V) 连通性、路径搜索
BFS O(V + E) O(V) 最短路径、层级遍历

💡 说明:V是顶点数,E是边数

🚀 优化技巧

DFS优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* 🎯 DFS优化技巧
*/
public class DFSOptimization {

// 简单的树节点定义
static class Node {
int value;
List<Node> children;

public Node(int value) {
this.value = value;
}
}

// 1️⃣ 剪枝优化 - 提前终止
private boolean dfsWithPruning(Node node, int target, int currentSum) {
// 🛑 剪枝:当前和已经超过目标
if (currentSum > target) return false;

// 🎯 找到解,提前终止
if (currentSum == target) return true;

// 🔄 继续搜索
for (Node child : node.children) {
if (dfsWithPruning(child, target, currentSum + child.value)) {
return true; // 🎯 提前终止
}
}

return false;
}

// 2️⃣ 记忆化搜索 - 避免重复计算
private Map<String, Boolean> memo = new HashMap<>();

private boolean dfsWithMemo(String state) {
// ✅ 检查是否已经计算过
if (memo.containsKey(state)) {
return memo.get(state);
}

// 🎯 基础情况(示例实现)
if (isBaseCase(state)) {
memo.put(state, true);
return true;
}

// 🔄 递归搜索
boolean result = false;
for (String nextState : getNextStates(state)) {
if (dfsWithMemo(nextState)) {
result = true;
break;
}
}

// 📝 记录结果
memo.put(state, result);
return result;
}

// 判断是否为基础情况(示例实现)
private boolean isBaseCase(String state) {
// 示例逻辑:空状态或特定终止状态
return state.isEmpty() || "goal".equals(state);
}

// 获取下一个可能的状态(示例实现)
private List<String> getNextStates(String state) {
// 示例逻辑:这里应该根据具体问题返回可能的下一个状态
// 实际使用时需要根据具体业务逻辑实现
return List.of();
}
}

BFS优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

/**
* 🎯 BFS优化技巧
*/
public class BFSOptimization {

// 1️⃣ 双向BFS - 从起点和终点同时开始
public int bidirectionalBFS(String beginWord, String endWord, Set<String> wordSet) {
Set<String> beginSet = new HashSet<>();
Set<String> endSet = new HashSet<>();
Set<String> visited = new HashSet<>(); // 记录已访问的单词

beginSet.add(beginWord);
endSet.add(endWord);
visited.add(beginWord);
visited.add(endWord);

int level = 1;

while (!beginSet.isEmpty() && !endSet.isEmpty()) {
// 🎯 总是从较小的集合开始扩展
if (beginSet.size() > endSet.size()) {
Set<String> temp = beginSet;
beginSet = endSet;
endSet = temp;
}

Set<String> nextLevel = new HashSet<>();

// 🔄 扩展当前层级
for (String word : beginSet) {
for (String nextWord : getNextWords(word, wordSet)) {
if (endSet.contains(nextWord)) {
return level + 1; // 🎯 双向相遇
}

if (!visited.contains(nextWord)) {
visited.add(nextWord);
nextLevel.add(nextWord);
}
}
}

beginSet = nextLevel;
level++;
}

return 0;
}

/**
* 📝 生成所有可能的下一个单词(改变一个字母)
*/
private List<String> getNextWords(String word, Set<String> wordSet) {
List<String> result = new LinkedList<>();
char[] chars = word.toCharArray();

// 🔄 尝试改变每个位置的字母
for (int i = 0; i < chars.length; i++) {
char original = chars[i];

// 🔄 尝试所有可能的字母
for (char c = 'a'; c <= 'z'; c++) {
if (c == original) continue; // ❌ 跳过原字母

chars[i] = c;
String newWord = new String(chars);

// ✅ 如果新单词在字典中,加入结果
if (wordSet.contains(newWord)) {
result.add(newWord);
}
}

chars[i] = original; // ↩️ 恢复原字母
}

return result;
}
}

🎯 经典LeetCode题目

📝 基础题目(⭐⭐☆☆☆)

题号 题目名称 算法类型 难度
200 岛屿数量 DFS ⭐⭐☆☆☆
102 二叉树的层序遍历 BFS ⭐⭐☆☆☆
104 二叉树的最大深度 DFS/BFS ⭐⭐☆☆☆
111 二叉树的最小深度 BFS ⭐⭐☆☆☆

📝 进阶题目(⭐⭐⭐☆☆)

题号 题目名称 算法类型 难度
127 单词接龙 BFS ⭐⭐⭐☆☆
130 被围绕的区域 DFS ⭐⭐⭐☆☆
417 太平洋大西洋水流问题 DFS/BFS ⭐⭐⭐☆☆
207 课程表 DFS/拓扑排序 ⭐⭐⭐☆☆

📝 高难度题目(⭐⭐⭐⭐☆)

题号 题目名称 算法类型 难度
126 单词接龙II BFS + DFS ⭐⭐⭐⭐☆
269 火星词典 拓扑排序 ⭐⭐⭐⭐☆
329 矩阵中的最长递增路径 DFS + 记忆化 ⭐⭐⭐⭐☆
444 序列重建 拓扑排序 ⭐⭐⭐⭐☆

🔄 多源BFS与单源BFS的对比与应用

📚 概念解析

单源BFS (Single Source BFS):从图中的单个起点出发,逐层遍历所有可达节点的搜索算法。

多源BFS (Multi Source BFS):从图中的多个起点同时出发,逐层扩展的搜索算法。适用于需要从多个起点同时进行广度优先搜索的场景。

📊 对比分析

特性 单源BFS 多源BFS
起点数量 1个 多个
初始化 队列中只放入一个起始节点 队列中放入所有起始节点
适用场景 最短路径、连通性检测等 多源最短路径、洪水填充等
时间复杂度 O(V+E) O(V+E)
空间复杂度 O(V) O(V)

🎯 应用场景

单源BFS的典型应用

  • 最短路径问题(无权图)
  • 层序遍历(树结构)
  • 连通性检测
  • 拓扑排序(结合入度)

多源BFS的典型应用

  • 疫情传播模型
  • 洪水填充算法
  • 多源最短距离(如房间中的灯照亮所有区域所需时间)
  • 矩阵中的01距离(计算每个1到最近0的距离)

💻 代码示例对比

单源BFS代码框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void singleSourceBFS(int[][] grid, int m, int[][] dist, int startX, int startY) {
// 将起始节点加入队列,并初始化距离为 0
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{startX, startY});
dist[startX][startY] = 0;

int[][] dirs = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
while (!queue.isEmpty()) {
int[] cur = queue.poll();
int x = cur[0], y = cur[1];
for (int[] dir : dirs) {
int nx = x + dir[0], ny = y + dir[1];
if (nx >= 0 && nx < m && ny >= 0 && ny < m) {
if (grid[nx][ny] != 2 && dist[nx][ny] == -1) {
dist[nx][ny] = dist[x][y] + 1;
queue.offer(new int[]{nx, ny});
}
}
}
}
}

多源BFS代码框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void multiSourceBFS(int[][] grid, int m, int[][] dist) {
Queue<int[]> queue = new LinkedList<>();

// 将所有起点(3)加入队列,并初始化距离为 0
for (int i = 0; i < m; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 3) {
dist[i][j] = 0;
queue.offer(new int[]{i, j});
}
}
}

int[][] dirs = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
while (!queue.isEmpty()) {
int[] cur = queue.poll();
int x = cur[0], y = cur[1];
for (int[] dir : dirs) {
int nx = x + dir[0], ny = y + dir[1];
if (nx >= 0 && nx < m && ny >= 0 && ny < m) {
if (grid[nx][ny] != 2 && dist[nx][ny] == -1) {
// 计算新距离
dist[nx][ny] = dist[x][y] + 1;
queue.offer(new int[]{nx, ny});
}
}
}
}
}

🚀 实战案例:矩阵中的01距离(LeetCode 542)

问题描述:给定一个由0和1组成的矩阵,找出每个1到最近的0的距离。

多源BFS解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 🏙️ 矩阵中的01距离(LeetCode 542)
*/
public class Matrix01 {
/**
* 🏙️ 计算每个1到最近0的距离(多源BFS解法)
*
* @param mat 输入矩阵
* @return 距离矩阵
*/
public int[][] updateMatrix(int[][] mat) {
if (mat == null || mat.length == 0) return new int[0][0];

int m = mat.length;
int n = mat[0].length;
int[][] dist = new int[m][n]; // 存储距离结果
Queue<int[]> queue = new LinkedList<>();

// 初始化:将所有0的位置加入队列(多源起点)
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (mat[i][j] == 0) {
queue.offer(new int[]{i, j});
dist[i][j] = 0;
} else {
dist[i][j] = -1; // 标记未访问
}
}
}

// 四个方向的偏移量
int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

// 多源BFS遍历
while (!queue.isEmpty()) {
int[] curr = queue.poll();
int x = curr[0];
int y = curr[1];

for (int[] dir : dirs) {
int nx = x + dir[0];
int ny = y + dir[1];

// 边界检查和未访问检查
if (nx >= 0 && nx < m && ny >= 0 && ny < n && dist[nx][ny] == -1) {
dist[nx][ny] = dist[x][y] + 1; // 距离等于父节点距离+1
queue.offer(new int[]{nx, ny});
}
}
}

return dist;
}
}

🧠 记忆口诀与技巧

🎯 DFS记忆口诀

1
2
3
4
5
6
7
🌊 DFS三步走:
1️⃣ 标记访问 ✅
2️⃣ 递归邻居 🔄
3️⃣ 回溯撤销 ↩️

💡 DFS精髓:
"一条路走到黑,不撞南墙不回头"

🎯 BFS记忆口诀

1
2
3
4
5
6
7
8
🌊 BFS四步曲:
1️⃣ 队列初始化 📋
2️⃣ 取出队首元素 📤
3️⃣ 处理并标记 ✅
4️⃣ 邻接顶点入队 📥

💡 BFS精髓:
"层层推进,稳扎稳打,保证最短"

🎨 选择策略口诀

1
2
3
4
5
6
🤔 算法选择口诀:

📏 要测距离用BFS,层层推进保最短
🗺️ 要探路径用DFS,深入到底再回退
⚡ 空间紧张用DFS,高度有限省空间
🎯 连通性问题两者皆可,看具体需求选算法

📖 参考资料

📚 中文资源

📚 经典教材

🎬 视频教程


🎓 学习总结

🎯 核心要点回顾

  1. DFS = 栈 + 递归 + 回溯,适合路径搜索
  2. BFS = 队列 + 层级 + 最短,适合距离计算
  3. 时间复杂度都是O(V+E),但空间复杂度不同
  4. 选择标准:看是否需要最短路径和层级信息

🚀 进阶学习路线

  1. 📚 基础巩固:熟练掌握DFS和BFS模板
  2. ⚡ 技巧提升:学习双向BFS、记忆化搜索等优化
  3. 🏗️ 实战应用:解决图论相关问题
  4. 🔬 算法扩展:学习拓扑排序、最短路径算法

💡 学习建议

1
2
3
4
5
📝 建议1:多画图辅助理解,可视化遍历过程
📝 建议2:熟练掌握两种算法的模板代码
📝 建议3:通过大量练习培养算法直觉
📝 建议4:理解适用场景,学会算法选择
📝 建议5:关注优化技巧,提升算法效率

最后更新:2025-01-15
作者:算法学习小分队