排序与 DAG

拓扑排序详解:附代码

有些事必须先于另一些发生:先安装再构建,先修课再上正课。拓扑排序把一张依赖之网理成一条你能跟着走的直线。下面讲它如何运作、为什么环会让它失效,以及如何用几行代码写出它。

11 分钟阅读 更新时间:2026 年 7 月 适合初学者
Mohammed Islam Hadjoudj
Mohammed Islam Hadjoudj
Expert Operations Research Engineer

什么是拓扑排序?

拓扑排序有向无环图(DAG)节点的一个线性排序,使得对每一条从 uv 的有向边,节点 u 都排在节点 v 之前。通俗地说:把一切排成一行,让每个箭头都指向前方。

这个定义里内置了两个条件。图必须是有向的(依赖有方向:A 在 B 前和 B 在 A 前并不相同),且是无环的(没有环)。如果 A 必须在 B 前、B 又必须在 A 前,就不可能存在有效的顺序。树和 DAG 在这里是近亲;关于树的一面,见有根树

你何时需要它

每当一个问题听起来像"按尊重前置条件的顺序做这些事"时,拓扑排序就是答案。你可以通过这些信号认出它:

它是技术面试中最常见的套路之一,因此出现在编程面试图算法指南中,也构成图论学习路线图的一个阶段。

Kahn 算法,分步讲解

最直观的方法是 Kahn 算法。它依赖每个节点的一个数字:入度,即入边的数量。入度为 0 的节点没有未满足的前置条件,因此可以安全地接下来放置它。

循环很简单:取任意入度为 0 的节点,输出它,并移除它的出边,这会降低其邻居的入度。重复直到什么都不剩。让我们在这个 DAG 上运行它。每个节点都标注了它在某个有效顺序中的位置。

A B C D E F 1 2 3 4 5 6
一个有效的拓扑排序:A、B、C、D、E、F。每个箭头都从较小的数字指向较大的数字。

下面是执行过程。队列保存入度已降到 0 的节点。我们从 A 开始,它是唯一不依赖任何东西的节点。

步骤输出队列(入度 0)
开始A
取 AAB, C
取 BA, BC
取 CA, B, CD, E
取 DA, B, C, DE
取 EA, B, C, D, EF
取 FA, B, C, D, E, F

注意,取走 A 之后,B 和 C 的入度同时降到了 0。两者都可以接下来出场,这正是为什么一个 DAG 通常有许多有效顺序。在实时图上看入度下降会让它豁然开朗,你可以在算法可视化工具中试一试。

Python 实现

Kahn 算法几乎可以直接翻译成代码。我们计算每个入度,用入度为零的节点初始化一个队列,然后把它清空。

from collections import deque, defaultdict

def topological_sort(num_nodes, edges):
    graph = defaultdict(list)
    in_degree = [0] * num_nodes

    for u, v in edges:          # 边 u -> v 表示 u 在 v 之前
        graph[u].append(v)
        in_degree[v] += 1

    # 用每个没有前置条件的节点初始化。
    queue = deque(n for n in range(num_nodes) if in_degree[n] == 0)
    order = []

    while queue:
        node = queue.popleft()
        order.append(node)
        for neighbour in graph[node]:
            in_degree[neighbour] -= 1        # 移除这条边
            if in_degree[neighbour] == 0:    # 不再有前置条件
                queue.append(neighbour)

    # 若某个节点始终没到达入度 0,是环把它挡住了。
    if len(order) == num_nodes:
        return order
    return []   # 检测到环,没有有效顺序

最后的检查是最优雅的部分:如果输出里缺了任何节点,那些节点就被困在一个环里。所以,同一段给 DAG 排序的代码,也检测出这个图究竟是不是 DAG。

DFS 方法

还有第二种基于深度优先搜索的经典方法。运行 DFS,当一个节点完成(它的所有后代都已探索)时,把它压入一个栈。拓扑排序就是把这个栈倒着读。

直觉是:一个节点只有在它所指向的一切都完成之后才会完成,因此在完成的逆序中,它排在它所有后代之前。DFS 版本无需记录入度,但你仍必须通过跟踪当前递归路径上的节点来防范环。两种方法同样有效;Kahn 的通常更容易推理,而 DFS 更紧凑。

环,以及它们为何让它失效

拓扑排序存在当且仅当图是无环的。原因立竿见影:环 A → B → A 要求 A 在 B 前、B 又在 A 前,同时成立,这在一条直线上是不可能的。

有用的推论:拓扑排序同时也是一个环检测器。在 Kahn 算法中,如果你无法输出全部 V 个节点,剩下的节点就构成至少一个环。在 DFS 版本中,遇到一个已经在当前路径上的节点,就意味着有环。

这就是为什么"课程表(Course Schedule)"类型的面试题,其实在问"这到底可不可能?",要用拓扑排序来解。

复杂度

两种算法都是最优的:它们对每个节点和每条边都恰好触碰一次。

方面开销原因
时间O(V + E)每个节点出队一次,每条边松弛一次
空间O(V)队列、入度数组和输出

正是这种线性开销,让拓扑排序能扩展到巨大的依赖图。要看它与其他所有图算法的对比,见复杂度指南和一页纸的速查表

现实应用

看着入度一个个下降

拓扑排序在动态中最容易领会:节点在最后一个前置条件消失的那一刻解锁。在实时图上一步步运行它。

打开算法可视化工具

常见问题

什么是拓扑排序?

拓扑排序是有向无环图(DAG)节点的一个线性排序,使得对每一条从 u 到 v 的有向边,u 在顺序中都排在 v 之前。它回答这样的问题:我该以什么顺序执行这些任务,才能让每个前置条件先完成?

拓扑排序用什么算法?

两种标准方法是:Kahn 算法,用队列反复移除入度为零的节点;以及深度优先搜索,按完成的逆序输出节点。两者都在 O(V + E) 时间内运行。

含环的图能做拓扑排序吗?

不能。拓扑排序只对有向无环图存在。如果图中有环,就不存在有效的顺序,而算法无法放置每个节点,正是你检测环的方式。

拓扑排序是唯一的吗?

通常不是。只要两个节点之间没有路径,它们就可以以任意先后出现,因此一个 DAG 往往有许多有效的拓扑排序。只有当图是一条单链时,才存在唯一的顺序。

进一步的学习资源

看见它,而不只是读它

当你看到一张依赖图理成一条直线的那一刻,它就说得通了。加载一个 DAG,按下播放,跟随顺序逐渐成形。

使用算法可视化工具进行练习