在这里插入图片描述

👋 大家好,欢迎来到我的技术博客!
💻 作为一名热爱 Java 与软件开发的程序员,我始终相信:清晰的逻辑 + 持续的积累 = 稳健的成长
📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。
🎯 本文将围绕数据结构与算法这个话题展开,希望能为你带来一些启发或实用的参考。
🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!


数据结构与算法 - BFS的核心逻辑:层次遍历与最短路径求解 🧠🗺️

在计算机科学的广袤天地中,图(Graph)和树(Tree)是两种极其重要的数据结构。它们不仅用于建模现实世界中的关系网络(如社交网络、交通路线、网页链接等),也是解决许多复杂问题的基石。而在图/树的遍历算法中,广度优先搜索(Breadth-First Search, 简称 BFS)以其独特的“逐层扩展”特性,成为处理层次遍历最短路径问题的首选工具。

本文将深入剖析 BFS 的核心逻辑,从基本思想、实现细节,到在树和图中的典型应用,再到最短路径问题的求解原理。我们将通过大量 Java 代码示例可视化图表(使用 Mermaid)以及真实场景类比,帮助你彻底掌握 BFS 的精髓。无论你是准备面试的开发者,还是正在学习算法的学生,这篇文章都将为你提供系统而深入的理解。


什么是 BFS?—— 从“涟漪”说起 💧

想象你向平静的湖面扔下一颗石子。水面会以石子落点为中心,一圈一圈地向外扩散波纹。这种“由近及远、层层推进”的现象,正是 BFS 的直观体现。

在 BFS 中,我们从一个起始节点(称为源点)出发,首先访问它所有直接相邻的节点(即距离为 1 的节点),然后再访问这些相邻节点的相邻节点(即距离为 2 的节点),依此类推,直到遍历完整个图或找到目标节点。

BFS 的本质:按“距离”递增的顺序访问节点。

这种特性使得 BFS 在以下两类问题中表现卓越:

  1. 层次遍历(Level-order Traversal):在树或图中按层访问节点。
  2. 无权图的最短路径(Shortest Path in Unweighted Graph):因为 BFS 第一次到达某节点时,走的路径一定是最短的。

BFS 的核心数据结构:队列 🧱

BFS 的实现离不开一个关键的数据结构:队列(Queue)。

队列遵循 FIFO(First In, First Out)原则——先进入队列的元素先被处理。这恰好符合 BFS “先访问近的,再访问远的”逻辑。

BFS 算法流程(通用版)

  1. 将起始节点加入队列,并标记为已访问。
  2. 当队列非空时:
    • 从队列头部取出一个节点(当前节点)。
    • 处理该节点(例如打印、记录路径等)。
    • 将该节点所有未被访问过的邻居节点加入队列,并标记为已访问。
  3. 重复步骤 2,直到队列为空。

⚠️ 注意:必须标记已访问节点,否则可能导致无限循环(尤其在图中存在环时)。


Java 实现 BFS 基础框架

下面是一个通用的 BFS 模板(适用于图):

import java.util.*;

public class BFSExample {
    // 图用邻接表表示:Map<Integer, List<Integer>>
    public static void bfs(Map<Integer, List<Integer>> graph, int start) {
        Queue<Integer> queue = new LinkedList<>();
        Set<Integer> visited = new HashSet<>();

        queue.offer(start);
        visited.add(start);

        while (!queue.isEmpty()) {
            int node = queue.poll();
            System.out.println("Visiting node: " + node); // 处理当前节点

            // 遍历所有邻居
            for (int neighbor : graph.getOrDefault(node, Collections.emptyList())) {
                if (!visited.contains(neighbor)) {
                    visited.add(neighbor);
                    queue.offer(neighbor);
                }
            }
        }
    }

    public static void main(String[] args) {
        // 构建一个简单图:0 -> [1, 2], 1 -> [3], 2 -> [3], 3 -> []
        Map<Integer, List<Integer>> graph = new HashMap<>();
        graph.put(0, Arrays.asList(1, 2));
        graph.put(1, Arrays.asList(3));
        graph.put(2, Arrays.asList(3));
        graph.put(3, Collections.emptyList());

        bfs(graph, 0);
    }
}

运行结果:

Visiting node: 0
Visiting node: 1
Visiting node: 2
Visiting node: 3

这个例子清晰地展示了 BFS 的层次性:先访问 0,再访问 1 和 2(同一层),最后访问 3(下一层)。


BFS 在二叉树中的应用:层次遍历 🌳

在二叉树中,BFS 通常被称为层次遍历(Level-order Traversal)。这是面试中的高频题型。

问题:按层打印二叉树节点

给定如下二叉树:

        3
       / \
      9   20
         /  \
        15   7

期望输出:[[3], [9,20], [15,7]]

Java 实现(带层级分组)

import java.util.*;

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode() {}
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

public class BinaryTreeLevelOrder {
    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 levelSize = queue.size(); // 当前层的节点数
            List<Integer> currentLevel = new ArrayList<>();

            for (int i = 0; i < levelSize; i++) {
                TreeNode node = queue.poll();
                currentLevel.add(node.val);

                if (node.left != null) queue.offer(node.left);
                if (node.right != null) queue.offer(node.right);
            }

            result.add(currentLevel);
        }

        return result;
    }

    public static void main(String[] args) {
        // 构建示例树
        TreeNode root = new TreeNode(3);
        root.left = new TreeNode(9);
        root.right = new TreeNode(20);
        root.right.left = new TreeNode(15);
        root.right.right = new TreeNode(7);

        BinaryTreeLevelOrder solver = new BinaryTreeLevelOrder();
        List<List<Integer>> levels = solver.levelOrder(root);
        System.out.println(levels); // [[3], [9, 20], [15, 7]]
    }
}

🔍 关键技巧:通过 queue.size() 获取当前层的节点数量,从而实现按层分组。


Mermaid 可视化:BFS 遍历过程 📊

下面用 Mermaid 图表展示上述二叉树的 BFS 遍历顺序:

3
9
20
15
7
  • 🟡 节点 3:第 0 层(起始)
  • 🔵 节点 9、20:第 1 层
  • 🟢 节点 15、7:第 2 层

BFS 严格按照这个顺序访问节点。


BFS 与最短路径:为什么它能保证最短? 🧭

这是 BFS 最强大的特性之一:在无权图(或边权相等的图)

直观理解

假设从起点 S 到终点 T 有两条路径:

  • 路径 A:S → A → T(长度 2)
  • 路径 B:S → B → C → T(长度 3)

BFS 会先探索所有距离为 1 的节点(A、B),再探索距离为 2 的节点(T、C)。因此,当 BFS 第一次访问到 T 时,走的是路径 A,长度为 2,这正是最短路径。

关键结论:BFS 第一次访问到某节点时,所经过的路径就是从起点到该节点的最短路径(在无权图中)。

数学证明(简略)

BFS 按距离 d = 0, 1, 2, … 的顺序访问节点。假设存在一条更短的路径未被 BFS 找到,那么该路径上的某个节点应在更早的层级被访问,矛盾。因此 BFS 找到的路径一定是最短的。


实战:LeetCode 经典题 —— 二进制矩阵中的最短路径 🧩

题目(LeetCode 1091):
给定一个 n x n 的二进制矩阵,0 表示可通过,1 表示障碍。从左上角 (0,0) 到右下角 (n-1,n-1),每次可向 8 个方向移动。求最短路径长度。若不可达,返回 -1。

分析

  • 这是一个网格图(Grid Graph),每个格子是节点,相邻格子(8方向)是边。
  • 边权均为 1(每步代价相同)→ 适用 BFS
  • 需要记录路径长度 → 在队列中存储 (row, col, distance)

Java 实现

import java.util.*;

public class ShortestPathInBinaryMatrix {
    public int shortestPathBinaryMatrix(int[][] grid) {
        if (grid == null || grid.length == 0 || grid[0][0] == 1) {
            return -1;
        }

        int n = grid.length;
        if (n == 1) return 1;

        // 8个方向
        int[][] directions = {
            {-1,-1}, {-1,0}, {-1,1},
            {0,-1},           {0,1},
            {1,-1},  {1,0},   {1,1}
        };

        Queue<int[]> queue = new LinkedList<>();
        queue.offer(new int[]{0, 0, 1}); // {row, col, distance}
        grid[0][0] = 1; // 标记为已访问(直接修改原数组节省空间)

        while (!queue.isEmpty()) {
            int[] current = queue.poll();
            int row = current[0];
            int col = current[1];
            int dist = current[2];

            for (int[] dir : directions) {
                int newRow = row + dir[0];
                int newCol = col + dir[1];

                if (newRow == n - 1 && newCol == n - 1) {
                    return dist + 1;
                }

                if (newRow >= 0 && newRow < n && newCol >= 0 && newCol < n && grid[newRow][newCol] == 0) {
                    grid[newRow][newCol] = 1; // 标记访问
                    queue.offer(new int[]{newRow, newCol, dist + 1});
                }
            }
        }

        return -1;
    }

    public static void main(String[] args) {
        ShortestPathInBinaryMatrix solver = new ShortestPathInBinaryMatrix();
        int[][] grid = {
            {0,0,0},
            {1,1,0},
            {1,1,0}
        };
        System.out.println(solver.shortestPathBinaryMatrix(grid)); // 输出: 4
    }
}

💡 优化技巧:直接修改 grid 数组将 0 改为 1 来标记访问,避免额外使用 visited 数组。


BFS vs DFS:何时选择 BFS? ⚖️

特性 BFS DFS
数据结构 队列(Queue) 栈(Stack)或递归
空间复杂度 O(w)(w 为最大宽度) O(h)(h 为最大深度)
最短路径 ✅ 无权图中最短路径 ❌ 不保证最短
内存消耗 宽图时可能很大 深图时可能栈溢出
适用场景 层次遍历、最短路径、拓扑排序(部分) 路径存在性、回溯、连通分量

📌 经验法则

  • 要找“最少步数”、“最短距离” → 选 BFS。
  • 要找“是否存在路径”、“所有路径” → 可考虑 DFS。

多源 BFS:从多个起点同时开始 🌐

有时问题不是从一个点出发,而是从多个点同时开始 BFS。例如:

  • 腐烂的橘子(LeetCode 994):多个腐烂橘子同时感染周围新鲜橘子。
  • 地图中的最近距离:多个目标点,求每个点到最近目标的距离。

示例:01 矩阵(LeetCode 542)

给定一个 01 矩阵,对每个 1,计算到最近 0 的距离。

思路:将所有 0 作为起点,同时入队,进行多源 BFS。

public class UpdateMatrix {
    public int[][] updateMatrix(int[][] mat) {
        int m = mat.length, n = mat[0].length;
        Queue<int[]> queue = new LinkedList<>();
        int[][] directions = {{0,1},{0,-1},{1,0},{-1,0}};

        // 初始化:所有0入队,1设为-1表示未访问
        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});
                } else {
                    mat[i][j] = -1;
                }
            }
        }

        while (!queue.isEmpty()) {
            int[] cell = queue.poll();
            int row = cell[0], col = cell[1];

            for (int[] dir : directions) {
                int newRow = row + dir[0];
                int newCol = col + dir[1];

                if (newRow >= 0 && newRow < m && newCol >= 0 && newCol < n && mat[newRow][newCol] == -1) {
                    mat[newRow][newCol] = mat[row][col] + 1;
                    queue.offer(new int[]{newRow, newCol});
                }
            }
        }

        return mat;
    }
}

🌟 多源 BFS 的本质:将多个源点视为“同一层”的起点,后续扩展逻辑不变。


BFS 在图论中的其他应用 🧩

1. 拓扑排序(Topological Sort)

虽然通常用 DFS 实现,但 BFS 也可用于拓扑排序(Kahn 算法):

  • 计算每个节点的入度。
  • 将入度为 0 的节点入队。
  • 依次出队,移除其出边,若邻居入度变为 0,则入队。

2. 连通分量(Connected Components)

在无向图中,BFS 可用于找出所有连通分量:

public void findConnectedComponents(Map<Integer, List<Integer>> graph) {
    Set<Integer> visited = new HashSet<>();
    int componentId = 0;

    for (int node : graph.keySet()) {
        if (!visited.contains(node)) {
            bfsComponent(graph, node, visited);
            System.out.println("Component " + (++componentId) + " done.");
        }
    }
}

private void bfsComponent(Map<Integer, List<Integer>> graph, int start, Set<Integer> visited) {
    Queue<Integer> queue = new LinkedList<>();
    queue.offer(start);
    visited.add(start);

    while (!queue.isEmpty()) {
        int node = queue.poll();
        for (int neighbor : graph.getOrDefault(node, Collections.emptyList())) {
            if (!visited.contains(neighbor)) {
                visited.add(neighbor);
                queue.offer(neighbor);
            }
        }
    }
}

性能分析:时间与空间复杂度 ⏱️

  • 时间复杂度:O(V + E)
    V:节点数,E:边数。每个节点和每条边最多被访问一次。

  • 空间复杂度:O(V)
    队列最多存储所有节点(如完全图的第一层),visited 集合也需 O(V)。

📊 对于树:E = V - 1 → 时间复杂度 O(V)


常见陷阱与调试技巧 🛠️

  1. 忘记标记已访问 → 无限循环。
  2. 在错误时机标记访问:应在入队时标记,而非出队时。
    • ❌ 错误:出队后才标记 → 同一节点可能多次入队。
    • ✅ 正确:入队时立即标记。
  3. 边界条件遗漏:如空图、单节点、起点即终点。
  4. 方向数组写错:尤其在网格问题中,8方向 vs 4方向。

扩展阅读与资源 🔗

如果你希望进一步深入学习 BFS 及其变种,以下资源值得参考:

✅ 所有链接均经过测试,可正常访问(截至 2025 年 10 月)。


总结:BFS 的核心价值 🎯

BFS 不仅仅是一个遍历算法,它是一种按距离扩展的思维模式。掌握 BFS,意味着你拥有了处理以下问题的强大工具:

  • 树的层次遍历
  • 无权图的最短路径
  • 网格中的最少步数问题
  • 多源扩散模拟(如火灾蔓延、病毒感染)
  • 图的连通性分析

通过本文的系统讲解、Java 代码实现、Mermaid 可视化以及实战例题,相信你已经对 BFS 的核心逻辑有了深刻理解。记住:队列是 BFS 的心脏,层次是 BFS 的灵魂,最短路径是 BFS 的荣耀

现在,打开你的 IDE,亲手实现一个 BFS 吧!🚀


🙌 感谢你读到这里!
🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。
💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友!
💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿
🔔 关注我,不错过下一篇干货!我们下期再见!✨

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐