拓扑排序 - 处理有依赖关系的排序|Topological Sort

什么是拓扑排序?

拓扑排序(Topological Sort)是对有向无环图(DAG, Directed Acyclic Graph) 中的顶点进行排序的算法,使得所有有向边都从排在前面的顶点指向后面的顶点。

简单来说:如果任务 A 依赖任务 B,那么 B 必须排在 A 前面。拓扑排序就是找出这个先后顺序的算法。

注意:只有 DAG 才有拓扑排序,图中存在环则无法排序。


Kahn 算法(BFS 方法)

最常用的拓扑排序算法是 Kahn 算法,核心思想是:

  1. 计算每个顶点的入度(有多少边指向它)
  2. 将所有入度为 0 的顶点加入队列(这些是没有依赖的起始任务)
  3. 从队列取出一个顶点,将其加入排序结果
  4. 将该顶点指向的所有顶点的入度减 1
  5. 如果有顶点的入度变为 0,则加入队列
  6. 重复 3-5,直到队列为空
from collections import deque

def topological_sort(n, edges):
    """
    n: 顶点数
    edges: 边列表,每条边为 [from, to],表示 to 依赖于 from
    """
    # 构建邻接表和入度数组
    adj = [[] for _ in range(n)]
    indegree = [0] * n
    
    for from_node, to_node in edges:
        adj[from_node].append(to_node)
        indegree[to_node] += 1
    
    # 入度为0的顶点入队
    queue = deque([i for i in range(n) if indegree[i] == 0])
    result = []
    
    while queue:
        node = queue.popleft()
        result.append(node)
        for neighbor in adj[node]:
            indegree[neighbor] -= 1
            if indegree[neighbor] == 0:
                queue.append(neighbor)
    
    # 如果有环,队列会提前变空
    if len(result) != n:
        return "图中存在环,无法拓扑排序"
    
    return result

# 示例:课程选修顺序
# 课程0 -> 课程1 -> 课程2
edges = [
    [0, 1],  # 课程1依赖于课程0
    [1, 2]   # 课程2依赖于课程1
]
print(topological_sort(3, edges))
# 输出: [0, 1, 2]

DFS 方法(深度优先遍历)

另一种拓扑排序方法是基于 DFS 的后序遍历反转

def topological_sort_dfs(n, edges):
    adj = [[] for _ in range(n)]
    for from_node, to_node in edges:
        adj[from_node].append(to_node)
    
    visited = [0] * n  # 0=未访问, 1=访问中, 2=已完成
    result = []
    
    def dfs(node):
        if visited[node] == 1:  # 遇到"访问中"的节点,说明有环
            return False
        if visited[node] == 2:  # 已完成,直接返回
            return True
        
        visited[node] = 1  # 标记为访问中
        for neighbor in adj[node]:
            if not dfs(neighbor):
                return False
        
        visited[node] = 2  # 标记为已完成
        result.append(node)  # 后序加入结果
        return True
    
    for i in range(n):
        if not dfs(i):
            return "图中存在环"
    
    result.reverse()  # 反转后得到拓扑序
    return result

# 测试
edges = [[0,1], [1,2]]
print(topological_sort_dfs(3, edges))
# 输出: [0, 1, 2]

复杂度分析

指标 Kahn 算法 DFS 方法
时间复杂度 O(V + E) O(V + E)
空间复杂度 O(V + E) O(V + E)

其中 V 是顶点数,E 是边数。


经典应用场景

  1. 课程选修顺序(Course Schedule):大学课程有先修要求,计算选课顺序
  2. 项目构建顺序(Build System):Makefile、Gradle 等构建工具分析依赖顺序
  3. 任务调度(Task Scheduling):有依赖关系的任务并行调度
  4. 源代码依赖顺序:头文件的包含顺序
  5. 病毒检测(Family tree):家族血统的拓扑排序

总结

拓扑排序是处理有依赖关系任务排序的利器。核心是识别”入度为0”的起始任务,然后逐步消除依赖。Kahn 算法直观易懂,DFS 方法更优雅。两者时间复杂度相同,都是 O(V+E)。