🗺️ 图的遍历(DFS & BFS):探索迷宫的两种方式!

65 阅读11分钟

"深度优先走到底,广度优先层层推!" 🎯


📖 一、什么是图的遍历?从走迷宫说起

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

步骤详解:
124 (到底了,回溯)
    ↑
    └─ 5 (到底了,回溯)
    ↑
    └─ 36 (到底了,完成)

遍历结果: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) (到底,返回)
  ← 回到1dfs(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遍历顺序:

0112, 324, 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的区别?

答案:

  1. 数据结构:DFS用栈,BFS用队列
  2. 遍历顺序:DFS深度优先,BFS层次优先
  3. 空间复杂度:DFS是O(h),BFS是O(w)
  4. 最短路径:BFS保证找到最短路径,DFS不保证
  5. 应用: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张纸记路

结论: 时间和空间的权衡!🎯


📚 八、知识点总结

核心要点 ✨

  1. DFS

    • 深度优先,一条路走到底
    • 用栈或递归
    • 空间O(h)
    • 适合路径判断
  2. 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:
    一路到底          层层推进
       ↓                 ↓
      栈               队列
       ↓                 ↓
    路径判断          最短路径

继续加油,下一个知识点见! 💪😄


📖 参考资料

  1. 《算法导论》第22章 - 图的基本算法
  2. LeetCode图专题
  3. 《算法(第4版)》- 图遍历
  4. GeeksforGeeks - DFS & BFS

作者: AI算法导师
最后更新: 2025年11月
难度等级: ⭐⭐⭐⭐ (中高级)
预计学习时间: 3-4小时

💡 温馨提示:DFS和BFS是图论的基础,建议多做LeetCode相关题目加深理解!