Jimdeng's Blog

Back

DAG 有向无环图与拓扑排序

什么是 DAG#

DAG(Directed Acyclic Graph,有向无环图)是一种特殊的有向图,它满足两个条件:

  1. 有向:每条边都有方向,从起点指向终点
  2. 无环:图中不存在从某个顶点出发,沿着边方向最终又回到该顶点的路径
    ┌───┐     ┌───┐     ┌───┐
    │ A │ ──▶ │ B │ ──▶ │ D │
    └───┘     └───┘     └───┘
      │         │         ▲
      │         ▼         │
    ┌───┐     ┌───┐       │
    │ C │ ──▶ │ E │ ──────┘
    └───┘     └───┘
plain

上图就是一个典型的 DAG:边有方向,且无论从哪个节点出发都无法回到自身。

DAG 的关键性质#

  • 拓扑序存在:DAG 一定存在至少一种拓扑排序
  • 可检测环:如果一个有向图能完成拓扑排序,则它是 DAG;反之存在环
  • 最长路径可解:在 DAG 上求最长路径是多项式可解的(一般图上是 NP-hard)
  • 偏序关系:DAG 天然表示元素间的偏序(partial order)关系

什么是拓扑排序#

拓扑排序(Topological Sort)是将 DAG 的所有顶点排成一个线性序列,使得对图中的任意一条有向边 (u, v),u 在序列中都出现在 v 之前。

简单来说:如果 A 指向 B,那么 A 必须排在 B 前面。

经典应用场景#

场景依赖关系
课程安排课程 B 的先修课是课程 A
编译依赖模块 B 依赖模块 A 的编译产物
任务调度任务 B 必须在任务 A 完成后才能开始
包管理包 B 依赖包 A,安装时需先装 A
数据处理管道ETL 任务间的执行先后

[!note] 拓扑排序的结果不唯一。一个 DAG 可能有多种合法的拓扑序列。

算法一:Kahn 算法(BFS)#

Kahn 算法的核心思想是不断移除入度为 0 的节点:

  1. 计算所有节点的入度
  2. 将入度为 0 的节点入队
  3. 每次从队列取出一个节点,加入结果序列
  4. 将该节点的所有邻居入度减 1,若邻居入度变为 0 则入队
  5. 重复直到队列为空

如果最终结果序列包含所有节点,说明是 DAG;否则图中存在环。

Go 实现#

复杂度分析#

维度复杂度
时间复杂度(O(V + E))
空间复杂度(O(V + E))

其中 (V) 是顶点数,(E) 是边数。每个节点和每条边各被处理一次。

算法二:DFS 深度优先回溯#

DFS 方法的核心思想是后序遍历的逆序:

  1. 对每个未访问的节点执行 DFS
  2. 在 DFS 递归返回时(即节点的所有邻居都已访问完毕),将节点压入栈
  3. 最终栈的弹出顺序就是拓扑排序结果

环检测#

在 DFS 过程中维护三种状态:

  • 0(未访问):节点尚未被访问
  • 1(访问中):节点正在被访问(在当前递归栈中)
  • 2(已完成):节点及其所有后继都已访问完毕

如果在 DFS 中遇到状态为 1 的节点,说明存在环(当前递归路径上出现了回头边)。

Go 实现#

复杂度分析#

维度复杂度
时间复杂度(O(V + E))
空间复杂度(O(V))(递归栈 + 状态表)

两种算法对比#

对比维度Kahn 算法(BFS)DFS 算法
核心思想逐步删除入度为 0 的节点后序遍历的逆序
实现方式迭代 + 队列递归 + 栈
环检测结果节点数 < 总节点数则有环递归中遇到 visiting 状态则有环
输出特性倾向于输出”层级靠前”的节点优先倾向于输出深层依赖链尾端的节点优先
适合场景需要按”层级”输出、并行调度需要检测环、求关键路径
递归深度无递归,适合大规模图深图可能栈溢出

[!tip] 如果需要字典序最小的拓扑排序,只需将 Kahn 算法中的普通队列替换为优先队列(最小堆),每次取出编号最小的入度为 0 的节点即可。

实战:课程表问题#

LeetCode 207「课程表」是拓扑排序的经典应用:

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1。给你一个数组 prerequisites,其中 prerequisites[i] = [a, b] 表示如果要选课程 a 必须先选课程 b。判断是否可能完成所有课程的学习。

DAG 的更多应用#

关键路径(Critical Path)#

在项目管理中,DAG 用于表示任务依赖关系。关键路径是从起点到终点的最长路径,决定了项目的最短完成时间。计算关键路径需要先做拓扑排序,再按拓扑序动态规划求最长路径。

其他典型应用#

  • Git 提交历史:Git 的 commit DAG 天然是有向无环图
  • Spark/DAG 调度:大数据计算引擎的任务调度
  • 区块链 DAG:IOTA 等项目用 DAG 替代传统链式结构
  • 依赖注入容器:Spring 等框架用拓扑排序决定 Bean 的初始化顺序

总结#

要点说明
DAG 定义有向 + 无环的图,一定存在拓扑序
拓扑排序将 DAG 节点排成线性序列,满足所有边的方向约束
Kahn 算法BFS 思路,不断移除入度为 0 的节点,天然支持环检测
DFS 算法后序遍历逆序,通过三色标记法检测环
时间复杂度两种算法均为 (O(V+E))
核心应用任务调度、依赖解析、课程安排、关键路径

拓扑排序是图论中最实用的算法之一,理解了”入度为 0 先处理”和”后序逆序”这两个核心思想,就能轻松应对各种依赖关系问题。

DAG 有向无环图与拓扑排序
https://jimdeng.com/blog/dag-topological-sort
Author jimdeng
Published at July 12, 2026