拓扑排序:从课程表到构建依赖

有向无环图上的线性扩展:先修课、make、包依赖,背后都是同一套结构。

拓扑排序给出有向无环图(DAG)顶点的一个线性次序,使得每条边 u → v 都满足 u 出现在 v 之前。有环则无解——这也是检测循环依赖的经典手段。

Kahn 算法(按入度)

反复取出入度为 0 的节点,删边并更新邻居入度。可用队列实现;若最终输出节点数少于总数,则存在环。

func topoKahn(n int, edges [][]int) ([]int, bool) {
    g := make([][]int, n)
    indeg := make([]int, n)
    for _, e := range edges {
        g[e[0]] = append(g[e[0]], e[1])
        indeg[e[1]]++
    }
    q := []int{}
    for i, d := range indeg {
        if d == 0 {
            q = append(q, i)
        }
    }
    order := make([]int, 0, n)
    for len(q) > 0 {
        u := q[0]
        q = q[1:]
        order = append(order, u)
        for _, v := range g[u] {
            indeg[v]--
            if indeg[v] == 0 {
                q = append(q, v)
            }
        }
    }
    return order, len(order) == n
}

DFS 后序翻转

对每个未访问节点 DFS,在回溯时压栈;最后反转即为一种拓扑序。环检测可用「访问中」三色标记。

工程里的坑

  • 边的方向:是「A 依赖 B」还是「A 完成后才能 B」?建图前先统一语义。
  • 并列节点顺序:拓扑序不唯一;需要稳定输出时,对入度 0 集合排序。
  • 动态依赖:CI 里条件任务会改变图,不能一次排完就固化。

小结

课程表、镜像构建层、Terraform 资源依赖——看见「必须先完成 A 再 B」,就想到 DAG 与拓扑序。Kahn 直观好调试,DFS 在递归结构里也很自然。