4、DAG图论基础:有向无环图定义、拓扑排序原理、邻接表与邻接矩阵表示法

在任务调度系统中,任务之间的依赖关系天然构成一张有向图。而有向无环图(DAG)是调度系统能够正确执行的前提——一旦图中出现环,调度将陷入死锁或无限循环。本节从图论基础出发,系统讲解DAG的定义、拓扑排序原理以及两种核心存储结构。

4.1 有向无环图(DAG)定义

有向无环图(Directed Acyclic Graph,DAG) 是一个由顶点(Vertex)和有向边(Directed Edge)组成的图,且图中不存在任何有向环。在任务调度场景中:

  • 顶点:代表一个任务节点(Job / Task)。
  • 有向边:代表依赖关系,如 A → B 表示 B 必须在 A 完成后才能执行。
核心性质: DAG 保证所有任务可以按照某种线性顺序执行,且不违反依赖约束。这是拓扑排序存在的前提。
特性 说明 调度意义
有向性 边具有方向,表示依赖流向 任务执行顺序受方向约束
无环性 不存在从某顶点出发又回到自身的路径 避免死锁,保证调度可终止
偏序关系 部分顶点间存在先后顺序 支持并行执行无依赖的任务
DAG 结构示例(任务依赖关系) 任务A 任务B 任务C 任务D 任务E 任务F 无环路径 A→B→E→F

4.2 拓扑排序原理

拓扑排序(Topological Sorting) 是对 DAG 所有顶点的一种线性排序,使得对于每条有向边 U → V,顶点 U 在排序中都出现在 V 之前。拓扑排序是任务调度引擎计算执行顺序的核心算法。

4.2.1 Kahn 算法(基于入度)

Kahn 算法通过维护每个顶点的入度(In-degree)来逐步剥离无依赖节点:

  1. 计算所有顶点的入度。
  2. 将所有入度为 0 的顶点加入队列。
  3. 从队列中取出一个顶点,输出并删除其所有出边,同时更新目标顶点的入度。
  4. 若目标顶点入度变为 0,则加入队列。
  5. 重复步骤 3-4,直到队列为空。
调度应用: Kahn 算法天然适合任务调度——入度为 0 的顶点代表“当前可执行任务”,队列即“就绪队列”。每次出队即调度一个任务,更新依赖后产生新的就绪任务。

4.2.2 DFS 后序法

基于深度优先搜索(DFS)的拓扑排序:对图进行 DFS 遍历,在递归返回时将顶点加入结果栈,最终栈的逆序即为拓扑排序。该方法需要检测环:若 DFS 过程中遇到当前路径上的顶点,则说明存在环。

算法 核心思想 时间复杂度 调度场景适用性
Kahn 算法 入度归零 + 队列 O(V + E) 适合实时调度,可动态获取就绪任务
DFS 后序法 递归回溯 + 栈 O(V + E) 适合静态分析,一次性生成全序

4.3 邻接表与邻接矩阵表示法

在代码实现中,DAG 的存储结构直接影响拓扑排序的性能和内存开销。两种最经典的表示法为邻接表(Adjacency List)和邻接矩阵(Adjacency Matrix)。

4.3.1 邻接表

邻接表为每个顶点维护一个链表或列表,存储其所有直接后继顶点。在任务调度中,邻接表可以同时存储入度信息,便于 Kahn 算法快速更新。

// Java 邻接表表示 DAG(适用于 Spring Boot 调度引擎)
public class DAGraph {
    private int vertexCount;
    private List<List<Integer>> adjList;   // 邻接表
    private int[] inDegree;                 // 入度数组

    public DAGraph(int n) {
        this.vertexCount = n;
        adjList = new ArrayList<>(n);
        inDegree = new int[n];
        for (int i = 0; i < n; i++) {
            adjList.add(new ArrayList<>());
        }
    }

    public void addEdge(int from, int to) {
        adjList.get(from).add(to);
        inDegree[to]++;
    }

    public List<Integer> topologicalSort() {
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < vertexCount; i++) {
            if (inDegree[i] == 0) queue.offer(i);
        }
        List<Integer> result = new ArrayList<>();
        while (!queue.isEmpty()) {
            int u = queue.poll();
            result.add(u);
            for (int v : adjList.get(u)) {
                if (--inDegree[v] == 0) queue.offer(v);
            }
        }
        return result;
    }
}

4.3.2 邻接矩阵

邻接矩阵使用一个 V × V 的二维布尔数组(或整型数组)表示顶点间的连接关系。matrix[i][j] = 1 表示存在从 i 到 j 的有向边。邻接矩阵适合稠密图,但空间复杂度为 O(V²),在任务数较多时开销较大。

// 邻接矩阵表示 DAG
public class DAGMatrix {
    private int vertexCount;
    private int[][] matrix;   // 邻接矩阵
    private int[] inDegree;

    public DAGMatrix(int n) {
        this.vertexCount = n;
        matrix = new int[n][n];
        inDegree = new int[n];
    }

    public void addEdge(int from, int to) {
        matrix[from][to] = 1;
        inDegree[to]++;
    }

    // 拓扑排序实现与邻接表类似,但遍历后继时需扫描整行
    public List<Integer> topologicalSort() {
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < vertexCount; i++) {
            if (inDegree[i] == 0) queue.offer(i);
        }
        List<Integer> result = new ArrayList<>();
        while (!queue.isEmpty()) {
            int u = queue.poll();
            result.add(u);
            for (int v = 0; v < vertexCount; v++) {
                if (matrix[u][v] == 1) {
                    if (--inDegree[v] == 0) queue.offer(v);
                }
            }
        }
        return result;
    }
}
邻接表 vs 邻接矩阵(DAG 存储结构对比) 邻接表 0: A → B → C 1: B → D 2: C → D → E 3: D → F 4: E → F 5: F (空) 邻接矩阵 A B C D E F A 1 1 B 1 C 1 1 D 1 E 1 F 邻接表:空间 O(V+E),适合稀疏图;邻接矩阵:空间 O(V²),适合稠密图或快速查边

4.4 存储结构选型对比

维度 邻接表 邻接矩阵
空间复杂度 O(V + E) O(V²)
查询边是否存在 O(degree) O(1)
遍历所有后继 O(degree) O(V)
适合图类型 稀疏图(任务数多,依赖少) 稠密图(任务数少,依赖密集)
Kahn 算法适配度 高,直接遍历邻接列表更新入度 中,需扫描整行,V 较大时性能下降
Spring Boot 调度推荐 首选,内存友好,扩展性强 仅用于小规模静态 DAG(V < 100)
故障排查提示: 在 Spring Boot 任务调度系统中,若 DAG 规模超过 1000 节点且使用邻接矩阵,极易触发 OOM 或 GC 频繁告警。生产环境务必使用邻接表,并配合入度数组实现 O(V+E) 的拓扑排序。