"深度优先走到底,广度优先层层推!" 🎯
📖 一、什么是图的遍历?从走迷宫说起
1.1 生活中的场景
想象你在一个迷宫中,要找到出口:
方案1:深度优先搜索(DFS)
策略:一条路走到黑,走不通再回头
入口 → 走廊A → 房间1 → 死路!
↓ 回退
→ 房间2 → 继续深入
→ 走廊B
→ 出口!✅
特点:
- 优先往深处探索
- 遇到死路就回退(回溯)
- 像"盲人摸象"
方案2:广度优先搜索(BFS)
策略:先探索近的,再探索远的
第1层:入口
第2层:走廊A、走廊B、走廊C
第3层:房间1、房间2、房间3、房间4
第4层:发现出口!✅
特点:
- 一层一层探索
- 先找到的一定是最短路径
- 像"水波扩散"
1.2 专业定义
深度优先搜索(DFS - Depth-First Search):
- 沿着一条路径尽可能深入
- 遇到死路就回溯
- 使用栈或递归实现
广度优先搜索(BFS - Breadth-First Search):
- 先访问近的节点,再访问远的节点
- 层层推进
- 使用队列实现
🎨 二、DFS详解
2.1 DFS原理图解
图示例:
1
/ \
2 3
/ \ \
4 5 6
DFS遍历顺序(递归):
访问1 → 访问2 → 访问4 → 回溯到2 → 访问5 → 回溯到1 → 访问3 → 访问6
步骤详解:
1 → 2 → 4 (到底了,回溯)
↑
└─ 5 (到底了,回溯)
↑
└─ 3 → 6 (到底了,完成)
遍历结果:1, 2, 4, 5, 3, 6
树形展示:
1 ①
/ \
2② 3⑤
/ \ \
4③ 5④ 6⑥
数字表示访问顺序
2.2 DFS实现方式
方式1:递归实现(最常用)⭐
public class DFS {
// 图的邻接表表示
private List<List<Integer>> graph;
private boolean[] visited;
public void dfs(int start) {
visited = new boolean[graph.size()];
dfsRecursive(start);
}
private void dfsRecursive(int node) {
// 1. 访问当前节点
System.out.print(node + " ");
visited[node] = true;
// 2. 递归访问所有未访问的邻居
for (int neighbor : graph.get(node)) {
if (!visited[neighbor]) {
dfsRecursive(neighbor);
}
}
}
}
执行过程:
图:0 - 1 - 2
| |
3 - 4
调用栈变化:
dfs(0)
→ dfs(1)
→ dfs(2) (到底,返回)
← 回到1
→ dfs(4)
→ dfs(3) (到底,返回)
← 回到4
← 回到1
← 回到0
输出:0 1 2 4 3
方式2:栈实现(迭代)
public void dfsIterative(int start) {
boolean[] visited = new boolean[graph.size()];
Stack<Integer> stack = new Stack<>();
stack.push(start);
while (!stack.isEmpty()) {
int node = stack.pop();
if (!visited[node]) {
System.out.print(node + " ");
visited[node] = true;
// 将未访问的邻居压入栈
for (int neighbor : graph.get(node)) {
if (!visited[neighbor]) {
stack.push(neighbor);
}
}
}
}
}
2.3 DFS完整实现
import java.util.*;
public class DepthFirstSearch {
private List<List<Integer>> graph;
private int vertices;
public DepthFirstSearch(int v) {
this.vertices = v;
graph = new ArrayList<>(v);
for (int i = 0; i < v; i++) {
graph.add(new ArrayList<>());
}
}
// 添加边(无向图)
public void addEdge(int u, int v) {
graph.get(u).add(v);
graph.get(v).add(u);
}
// DFS递归
public void dfs(int start) {
boolean[] visited = new boolean[vertices];
System.out.print("DFS递归遍历:");
dfsRecursive(start, visited);
System.out.println();
}
private void dfsRecursive(int node, boolean[] visited) {
visited[node] = true;
System.out.print(node + " ");
for (int neighbor : graph.get(node)) {
if (!visited[neighbor]) {
dfsRecursive(neighbor, visited);
}
}
}
// DFS迭代(栈)
public void dfsIterative(int start) {
boolean[] visited = new boolean[vertices];
Stack<Integer> stack = new Stack<>();
System.out.print("DFS迭代遍历:");
stack.push(start);
while (!stack.isEmpty()) {
int node = stack.pop();
if (!visited[node]) {
visited[node] = true;
System.out.print(node + " ");
// 逆序添加邻居,保证遍历顺序一致
List<Integer> neighbors = graph.get(node);
for (int i = neighbors.size() - 1; i >= 0; i--) {
if (!visited[neighbors.get(i)]) {
stack.push(neighbors.get(i));
}
}
}
}
System.out.println();
}
// 测试
public static void main(String[] args) {
DepthFirstSearch dfs = new DepthFirstSearch(7);
// 构建图
dfs.addEdge(0, 1);
dfs.addEdge(0, 2);
dfs.addEdge(1, 3);
dfs.addEdge(1, 4);
dfs.addEdge(2, 5);
dfs.addEdge(2, 6);
System.out.println("图的结构:");
System.out.println(" 0");
System.out.println(" / \\");
System.out.println(" 1 2");
System.out.println(" / \\ / \\");
System.out.println("3 4 5 6");
System.out.println();
dfs.dfs(0);
dfs.dfsIterative(0);
}
}
输出:
图的结构:
0
/ \
1 2
/ \ / \
3 4 5 6
DFS递归遍历:0 1 3 4 2 5 6
DFS迭代遍历:0 1 3 4 2 5 6
🌊 三、BFS详解
3.1 BFS原理图解
同样的图:
1
/ \
2 3
/ \ \
4 5 6
BFS遍历顺序:
层0:1
层1:2, 3
层2:4, 5, 6
队列变化:
初始:[1]
弹出1,加入2,3:[2, 3]
弹出2,加入4,5:[3, 4, 5]
弹出3,加入6: [4, 5, 6]
弹出4: [5, 6]
弹出5: [6]
弹出6: []
遍历结果:1, 2, 3, 4, 5, 6
层次展示:
1 ① ← 第0层
/ \
2② 3③ ← 第1层
/ \ \
4④ 5⑤ 6⑥ ← 第2层
数字表示访问顺序
3.2 BFS实现(队列)
public class BFS {
private List<List<Integer>> graph;
public void bfs(int start) {
boolean[] visited = new boolean[graph.size()];
Queue<Integer> queue = new LinkedList<>();
// 1. 起点入队
queue.offer(start);
visited[start] = true;
// 2. 队列不空就继续
while (!queue.isEmpty()) {
int node = queue.poll();
System.out.print(node + " ");
// 3. 将所有未访问的邻居入队
for (int neighbor : graph.get(node)) {
if (!visited[neighbor]) {
queue.offer(neighbor);
visited[neighbor] = true;
}
}
}
}
}
执行过程:
图:0 - 1 - 2
| |
3 - 4
队列变化:
[0] → 访问0,加入1,3 → [1,3]
[1,3] → 访问1,加入2,4 → [3,2,4]
[3,2,4] → 访问3 → [2,4]
[2,4] → 访问2 → [4]
[4] → 访问4 → []
输出:0 1 3 2 4
3.3 BFS完整实现
import java.util.*;
public class BreadthFirstSearch {
private List<List<Integer>> graph;
private int vertices;
public BreadthFirstSearch(int v) {
this.vertices = v;
graph = new ArrayList<>(v);
for (int i = 0; i < v; i++) {
graph.add(new ArrayList<>());
}
}
public void addEdge(int u, int v) {
graph.get(u).add(v);
graph.get(v).add(u);
}
// BFS基础版
public void bfs(int start) {
boolean[] visited = new boolean[vertices];
Queue<Integer> queue = new LinkedList<>();
System.out.print("BFS遍历:");
queue.offer(start);
visited[start] = true;
while (!queue.isEmpty()) {
int node = queue.poll();
System.out.print(node + " ");
for (int neighbor : graph.get(node)) {
if (!visited[neighbor]) {
queue.offer(neighbor);
visited[neighbor] = true;
}
}
}
System.out.println();
}
// BFS分层遍历(记录层数)
public void bfsWithLevel(int start) {
boolean[] visited = new boolean[vertices];
Queue<Integer> queue = new LinkedList<>();
System.out.println("BFS分层遍历:");
queue.offer(start);
visited[start] = true;
int level = 0;
while (!queue.isEmpty()) {
int size = queue.size();
System.out.print("第" + level + "层:");
// 处理当前层的所有节点
for (int i = 0; i < size; i++) {
int node = queue.poll();
System.out.print(node + " ");
for (int neighbor : graph.get(node)) {
if (!visited[neighbor]) {
queue.offer(neighbor);
visited[neighbor] = true;
}
}
}
System.out.println();
level++;
}
}
// BFS求最短路径
public int shortestPath(int start, int end) {
if (start == end) return 0;
boolean[] visited = new boolean[vertices];
Queue<Integer> queue = new LinkedList<>();
queue.offer(start);
visited[start] = true;
int distance = 0;
while (!queue.isEmpty()) {
int size = queue.size();
distance++;
for (int i = 0; i < size; i++) {
int node = queue.poll();
for (int neighbor : graph.get(node)) {
if (neighbor == end) {
return distance;
}
if (!visited[neighbor]) {
queue.offer(neighbor);
visited[neighbor] = true;
}
}
}
}
return -1; // 不可达
}
// 测试
public static void main(String[] args) {
BreadthFirstSearch bfs = new BreadthFirstSearch(7);
bfs.addEdge(0, 1);
bfs.addEdge(0, 2);
bfs.addEdge(1, 3);
bfs.addEdge(1, 4);
bfs.addEdge(2, 5);
bfs.addEdge(2, 6);
System.out.println("图的结构:");
System.out.println(" 0");
System.out.println(" / \\");
System.out.println(" 1 2");
System.out.println(" / \\ / \\");
System.out.println("3 4 5 6");
System.out.println();
bfs.bfs(0);
System.out.println();
bfs.bfsWithLevel(0);
System.out.println();
System.out.println("0到6的最短距离:" + bfs.shortestPath(0, 6));
}
}
输出:
图的结构:
0
/ \
1 2
/ \ / \
3 4 5 6
BFS遍历:0 1 2 3 4 5 6
BFS分层遍历:
第0层:0
第1层:1 2
第2层:3 4 5 6
0到6的最短距离:2
🆚 四、DFS vs BFS对比
4.1 详细对比表
| 特性 | DFS(深度优先) | BFS(广度优先) |
|---|---|---|
| 数据结构 | 栈(或递归) | 队列 |
| 遍历顺序 | 深度优先 | 层次优先 |
| 空间复杂度 | O(h) h=树高 | O(w) w=最宽层 |
| 时间复杂度 | O(V+E) | O(V+E) |
| 最短路径 | ❌ 不保证 | ✅ 保证 |
| 实现难度 | 简单(递归) | 中等(队列) |
| 适用场景 | 路径存在性 拓扑排序 检测环 | 最短路径 层次遍历 社交网络 |
4.2 形象比喻
DFS:
像探险家进入洞穴:
1. 选一条路往深处走
2. 走到尽头就回头
3. 换另一条路继续
优点:可以深入探索
缺点:可能绕远路
BFS:
像水波扩散:
1. 从中心开始
2. 一圈一圈扩散
3. 先到近的后到远的
优点:一定找到最短路
缺点:需要记录很多节点
4.3 选择建议
选择DFS的场景:
- ✅ 判断路径是否存在
- ✅ 拓扑排序
- ✅ 检测图中的环
- ✅ 找所有可能的路径
- ✅ 回溯问题
选择BFS的场景:
- ✅ 求最短路径(无权图)
- ✅ 层次遍历
- ✅ 社交网络(几度好友)
- ✅ 找最少步数
🎯 五、经典应用题
5.1 岛屿数量(LeetCode 200)- DFS
// 给定二维网格,1表示陆地,0表示水,求岛屿数量
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) return 0;
int count = 0;
int rows = grid.length;
int cols = grid[0].length;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (grid[i][j] == '1') {
count++;
dfs(grid, i, j); // 将整个岛屿标记为已访问
}
}
}
return count;
}
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] != '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); // 右
}
5.2 二叉树的层序遍历(LeetCode 102)- BFS
// 返回二叉树的层序遍历结果
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) return result;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
List<Integer> level = new ArrayList<>();
// 处理当前层的所有节点
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
result.add(level);
}
return result;
}
5.3 单词接龙(LeetCode 127)- BFS
// 从beginWord变到endWord,每次只能改一个字母,求最短路径长度
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<>();
queue.offer(beginWord);
int level = 1;
while (!queue.isEmpty()) {
int size = queue.size();
level++;
for (int i = 0; i < size; i++) {
String word = queue.poll();
char[] chars = word.toCharArray();
// 尝试改变每个位置的字母
for (int j = 0; j < chars.length; j++) {
char old = chars[j];
for (char c = 'a'; c <= 'z'; c++) {
chars[j] = c;
String newWord = new String(chars);
if (newWord.equals(endWord)) {
return level;
}
if (wordSet.contains(newWord)) {
queue.offer(newWord);
wordSet.remove(newWord); // 避免重复访问
}
}
chars[j] = old; // 恢复
}
}
}
return 0;
}
5.4 课程表(LeetCode 207)- DFS检测环
// 判断能否完成所有课程(检测有向图是否有环)
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
graph.add(new ArrayList<>());
}
// 构建图
for (int[] pre : prerequisites) {
graph.get(pre[1]).add(pre[0]);
}
int[] visited = new int[numCourses]; // 0:未访问 1:访问中 2:已完成
// 检测每个节点
for (int i = 0; i < numCourses; i++) {
if (hasCycle(graph, i, visited)) {
return false;
}
}
return true;
}
private boolean hasCycle(List<List<Integer>> graph, int node, int[] visited) {
if (visited[node] == 1) return true; // 访问中,发现环
if (visited[node] == 2) return false; // 已完成,无环
visited[node] = 1; // 标记为访问中
for (int neighbor : graph.get(node)) {
if (hasCycle(graph, neighbor, visited)) {
return true;
}
}
visited[node] = 2; // 标记为已完成
return false;
}
5.5 腐烂的橘子(LeetCode 994)- BFS
// 每分钟腐烂的橘子会感染相邻的新鲜橘子,求所有橘子腐烂需要的时间
public int orangesRotting(int[][] grid) {
int rows = grid.length;
int cols = grid[0].length;
Queue<int[]> queue = new LinkedList<>();
int fresh = 0;
// 统计新鲜橘子数量,腐烂橘子入队
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (grid[i][j] == 2) {
queue.offer(new int[]{i, j});
} else if (grid[i][j] == 1) {
fresh++;
}
}
}
if (fresh == 0) return 0;
int minutes = 0;
int[][] dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
while (!queue.isEmpty()) {
int size = queue.size();
minutes++;
for (int i = 0; i < size; i++) {
int[] pos = queue.poll();
for (int[] dir : dirs) {
int x = pos[0] + dir[0];
int y = pos[1] + dir[1];
if (x >= 0 && x < rows && y >= 0 && y < cols && grid[x][y] == 1) {
grid[x][y] = 2;
fresh--;
queue.offer(new int[]{x, y});
}
}
}
}
return fresh == 0 ? minutes - 1 : -1;
}
🎓 六、经典面试题
面试题1:DFS和BFS的区别?
答案:
- 数据结构:DFS用栈,BFS用队列
- 遍历顺序:DFS深度优先,BFS层次优先
- 空间复杂度:DFS是O(h),BFS是O(w)
- 最短路径:BFS保证找到最短路径,DFS不保证
- 应用:DFS适合路径判断,BFS适合最短路径
面试题2:什么时候用DFS,什么时候用BFS?
答案:
DFS:
- 路径是否存在
- 拓扑排序
- 检测环
- 回溯问题
BFS:
- 最短路径
- 层次遍历
- 最少步数
面试题3:如何避免重复访问?
答案:
// 方法1:visited数组
boolean[] visited = new boolean[n];
visited[node] = true;
// 方法2:标记原数组(如岛屿问题)
grid[i][j] = '0'; // 将1标记为0
// 方法3:Set记录
Set<String> visited = new HashSet<>();
visited.add(word);
面试题4:DFS和递归的关系?
答案:
- DFS可以用递归实现(最简单)
- 也可以用栈实现(迭代)
- 递归本质上就是用系统栈
面试题5:BFS为什么能找到最短路径?
答案: 因为BFS是层次遍历:
- 第1层的节点距离是1
- 第2层的节点距离是2
- 最先到达目标的路径一定最短
🎪 七、趣味小故事
故事:两个探险家的冒险
从前,有两个探险家要寻找宝藏。
DFS探险家(深度优先的小明):
小明的策略:
入口 → 选一条路一直走
→ 遇到岔路选左边
→ 走到尽头没宝藏
→ 回退到岔路口
→ 换右边继续走
→ 终于找到宝藏!
特点:
- 路线曲折,走了很多弯路
- 但节省地图纸(空间小)
BFS探险家(广度优先的小红):
小红的策略:
入口 → 先探索所有相邻的房间
→ 再探索距离为2的房间
→ 再探索距离为3的房间
→ 很快找到宝藏!
特点:
- 路线最短,步数最少
- 但需要记录很多房间(空间大)
结果:
- 小明:走了100步,但只用了1张纸记路
- 小红:走了30步,但用了10张纸记路
结论: 时间和空间的权衡!🎯
📚 八、知识点总结
核心要点 ✨
-
DFS:
- 深度优先,一条路走到底
- 用栈或递归
- 空间O(h)
- 适合路径判断
-
BFS:
- 广度优先,层层推进
- 用队列
- 空间O(w)
- 适合最短路径
记忆口诀 🎵
DFS深度栈递归,
一条路上走到黑。
BFS广度用队列,
层层扩散找最短。
时间复杂都V加E,
空间一个h一个w。
路径判断用深搜,
最短距离用广搜!
模板代码 📝
DFS模板:
void dfs(Node node, Set<Node> visited) {
if (visited.contains(node)) return;
visited.add(node);
// 处理当前节点
for (Node neighbor : node.neighbors) {
dfs(neighbor, visited);
}
}
BFS模板:
void bfs(Node start) {
Queue<Node> queue = new LinkedList<>();
Set<Node> visited = new HashSet<>();
queue.offer(start);
visited.add(start);
while (!queue.isEmpty()) {
Node node = queue.poll();
// 处理当前节点
for (Node neighbor : node.neighbors) {
if (!visited.contains(neighbor)) {
queue.offer(neighbor);
visited.add(neighbor);
}
}
}
}
🌟 九、总结彩蛋
恭喜你!🎉 你已经掌握了图遍历的两大利器!
记住:
- 🔍 DFS:深度优先,递归或栈
- 🌊 BFS:广度优先,队列
- 🎯 根据场景选择合适的算法
- 💪 多刷题巩固理解
最后送你一张图
DFS: BFS:
一路到底 层层推进
↓ ↓
栈 队列
↓ ↓
路径判断 最短路径
继续加油,下一个知识点见! 💪😄
📖 参考资料
- 《算法导论》第22章 - 图的基本算法
- LeetCode图专题
- 《算法(第4版)》- 图遍历
- GeeksforGeeks - DFS & BFS
作者: AI算法导师
最后更新: 2025年11月
难度等级: ⭐⭐⭐⭐ (中高级)
预计学习时间: 3-4小时
💡 温馨提示:DFS和BFS是图论的基础,建议多做LeetCode相关题目加深理解!