跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava算法

BFS 实现拓扑排序:原理与 LeetCode 实战

有向无环图(DAG)是拓扑排序的基础,通过计算节点入度并利用队列进行广度优先搜索(BFS),可高效判断依赖关系或生成线性序列。解析了拓扑排序的核心流程,结合课程表(LeetCode 207/210)及火星词典(LCR 114)三个经典场景,演示了如何构建邻接表、统计入度、执行 BFS 遍历并检测环路。代码采用 Java 实现,包含完整的建图与判环逻辑,适合算法初学者深入理解图论应用。

清心发布于 2026/3/28更新于 2026/9/1059 浏览
BFS 实现拓扑排序:原理与 LeetCode 实战

一、拓扑排序基础

1.1 有向无环图(DAG)

有向无环图是指一个没有回路的有向图。简单来说,如果无法从某个顶点出发经过若干条边回到该点,那么这个图就是 DAG。

文章配图

1.2 AOV 网:顶点活动图

在 DAG 的基础上,我们用顶点表示活动,用边表示活动执行的先后顺序。

文章配图

1.3 什么是拓扑排序

拓扑排序的目标是找到做事的先后顺序。核心逻辑是:每次找出入度为 0 的点,将其加入结果序列,并删除该点相连的边,重复此过程直到所有点都被处理或发现环路。

1.4 基于 BFS 的实现思路

借助队列进行广度优先搜索(BFS)是实现拓扑排序的高效方式:

  1. 初始化:将所有入度为 0 的点加入队列。
  2. 循环处理:当队列不为空时,取出队头元素加入最终结果。
  3. 更新依赖:删除与该点相连的边(即减少相邻点的入度)。
  4. 入队判断:若某相邻点的入度变为 0,则将其加入队列。

二、LeetCode 207. 课程表

题目要求判断是否能完成所有课程的学习。给定课程总数和先修课程关系数组,我们需要验证是否存在合法的拓扑排序。

解题思路

使用邻接表 List<List<Integer>> 表示图结构,下标对应课程 ID,数组内容指向的课程即为依赖关系。遍历先修数组构建图的同时统计每个课程的入度。随后执行一次 BFS 即可判断可行性。

注意细节:

  • 每个点只能入队一次,避免重复计算。
  • 若先修数组为空,直接返回 true。

代码实现

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        int m = prerequisites.length;
        if (m == 0) return true;
        
        // 建图
        List<List<Integer>> edges = new ArrayList<>();
        for (int i = 0; i < numCourses; i++) {
            edges.add(new ArrayList<>());
        }
        
        Queue<Integer> queue = new LinkedList<>();
        int[] inDegree = new int[numCourses];
        boolean[] visited = new boolean[numCourses];
        
        // 统计入度并建图
        for (int i = 0; i < m; i++) {
            // 根据输入格式建立连接
            edges.get(prerequisites[i][prerequisites[i].length - 1]).add(prerequisites[i][0]);
            inDegree[prerequisites[i][0]]++;
        }
        
        // 入度为 0 的点入队
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) {
                queue.add(i);
                visited[i] = true;
            }
        }
        
        if (queue.isEmpty()) return false;
        
        // BFS 遍历
        while (!queue.isEmpty()) {
            int tmp = queue.poll();
            
            // 销毁边(减少邻居入度)
            for (int neighbor : edges.get(tmp)) {
                inDegree[neighbor]--;
            }
            
            // 新产生的入度为 0 的点入队
            for (int i = 0; i < numCourses; i++) {
                if (inDegree[i] == 0 && !visited[i]) {
                    queue.add(i);
                    visited[i] = true;
                }
            }
        }
        
        // 检查是否所有点都处理过
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] != 0) return false;
        }
        return true;
    }
}

三、LeetCode 210. 课程表 II

这道题不仅要求判断能否完成,还需要返回具体的学习顺序。如果无法完成,返回空数组。

解题思路

基本流程与上一题一致,区别在于需要记录出队的顺序。我们可以预先初始化结果数组,每弹出一个节点就填入结果中。最后比较已处理节点数与总课程数是否一致。

代码实现

class Solution {
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        int[] result = new int[numCourses];
        for (int i = 0; i < numCourses; i++) result[i] = i;
        
        int m = prerequisites.length;
        if (m == 0) return result;
        
        Queue<Integer> queue = new LinkedList<>();
        boolean[] visited = new boolean[numCourses];
        int[] inDegree = new int[numCourses];
        List<List<Integer>> edges = new ArrayList<>();
        
        for (int i = 0; i < numCourses; i++) {
            edges.add(new ArrayList<>());
        }
        
        for (int i = 0; i < m; i++) {
            edges.get(prerequisites[i][prerequisites[i].length - 1]).add(prerequisites[i][0]);
            inDegree[prerequisites[i][0]]++;
        }
        
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) {
                visited[i] = true;
                queue.add(i);
            }
        }
        
        int count = 0;
        while (!queue.isEmpty()) {
            int tmp = queue.poll();
            result[count++] = tmp;
            
            for (int neighbor : edges.get(tmp)) {
                inDegree[neighbor]--;
            }
            
            for (int i = 0; i < numCourses; i++) {
                if (inDegree[i] == 0 && !visited[i]) {
                    queue.add(i);
                    visited[i] = true;
                }
            }
        }
        
        return count != numCourses ? new int[0] : result;
    }
}

四、LCR 114. 火星词典

这是一道字符串处理的拓扑排序变体。通过对比相邻单词,推导出字符间的优先级关系,进而构建图并排序。

解题思路

  1. 建图:两层循环对比相邻字符串,找到第一个不同的字符,前者指向后者。
  2. 合法性检查:如果一个长字符串以短字符串开头且排在前面(如 "abc" 在 "ab" 前),则非法。
  3. 拓扑排序:对字符图执行 BFS,若结果长度不足则说明存在环。

代码实现

class Solution {
    Map<Character, Set<Character>> edges = new HashMap<>();
    Map<Character, Integer> inDegree = new HashMap<>();
    boolean flag;

    public String alienOrder(String[] words) {
        // 初始化入度
        for (String s : words) {
            for (int i = 0; i < s.length(); i++) {
                char ch = s.charAt(i);
                inDegree.put(ch, 0);
            }
        }

        // 建图
        for (int i = 0; i < words.length; i++) {
            for (int j = i + 1; j < words.length; j++) {
                add(words[i], words[j]);
                if (flag) return "";
            }
        }

        // 拓扑排序初始化
        Queue<Character> queue = new LinkedList<>();
        for (char ch : inDegree.keySet()) {
            if (inDegree.get(ch) == 0) queue.add(ch);
        }

        // BFS
        StringBuffer ret = new StringBuffer();
        while (!queue.isEmpty()) {
            char tmp = queue.poll();
            ret.append(tmp);
            if (!edges.containsKey(tmp)) continue;
            for (char ch : edges.get(tmp)) {
                inDegree.put(ch, inDegree.get(ch) - 1);
                if (inDegree.get(ch) == 0) queue.add(ch);
            }
        }

        // 检查是否有剩余入度不为 0 的点
        for (char ch : inDegree.keySet()) {
            if (inDegree.get(ch) != 0) return "";
        }
        return ret.toString();
    }

    private void add(String a, String b) {
        int i = 0;
        int n = Math.min(a.length(), b.length());
        for (; i < n; i++) {
            char ch1 = a.charAt(i);
            char ch2 = b.charAt(i);
            if (ch1 != ch2) {
                if (!edges.containsKey(ch1)) edges.put(ch1, new HashSet<>());
                if (!edges.get(ch1).contains(ch2)) {
                    edges.get(ch1).add(ch2);
                    inDegree.put(ch2, inDegree.get(ch2) + 1);
                }
                break;
            }
        }
        // 检查非法情况:例如 "abc" 在 "ab" 前面
        if (i == b.length() && a.length() > i) flag = true;
    }
}

目录

  1. 一、拓扑排序基础
  2. 1.1 有向无环图(DAG)
  3. 1.2 AOV 网:顶点活动图
  4. 1.3 什么是拓扑排序
  5. 1.4 基于 BFS 的实现思路
  6. 二、LeetCode 207. 课程表
  7. 解题思路
  8. 代码实现
  9. 三、LeetCode 210. 课程表 II
  10. 解题思路
  11. 代码实现
  12. 四、LCR 114. 火星词典
  13. 解题思路
  14. 代码实现

更多推荐文章

查看全部
  • 基于 Python 的旅行数据可视化与分析系统
  • Python-Skill Bridge 实现 Python 与 Virtuoso Skill 无缝连接
  • Dify 与 MySQL 集成实战:基于 MCP 协议的数据交互方案
  • 春晚机器人热,股市为何不买账?
  • 网络安全入门指南:从基础原理到实战进阶
  • C++ AVL 树:概念、结构与旋转实现
  • 电影推荐与票房预测系统:基于 Python + Flask + 机器学习
  • RAG:大模型时代的检索增强生成技术
  • Python 爬虫实战:爬取微信公众号历史推文
  • WhisperLive:实时语音转文字解决方案
  • Kubernetes: 使用 kubectl 插件 ketall 查看所有 API 对象资源
  • 数据结构:树与堆
  • 网络安全学习平台盘点:七个从新手到进阶的资源
  • 滑动窗口算法结合例题详解
  • Ubuntu 24.04 LTS 配置清华大学镜像源加速下载与更新
  • 二叉树前中后序遍历详解:递归与迭代实现
  • 网络安全行业职业发展路径与技能要求详解
  • Python 兼职方向与接单指南:从入门到实战
  • 基于模型上下文协议(MCP)的可插拔式临床 AI 工具链研究
  • 3-RPS 并联机器人运动仿真与轨迹控制

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online