拓扑排序

知识讲解

拓扑排序(Topological Sorting):一种对 有向无环图(DAG) 的所有顶点进行线性排序的方法,使得图中任意一点 uv, 如果存在有向边 <u,v>, 则 u 必须在 v 之前出现,对有向图进行拓扑排序产生的线性序列称为满足拓扑次序的序列,简称拓扑排序。

图的拓扑排序是针对有向无环图(DAG)来说的,无向图和有向有环图没有拓扑排序,或者说不存在拓扑排序。

alt text

一个图可能存在多个拓扑序列,比如上图的拓扑序列可以是:

\[V_1 \rightarrow V_2 \rightarrow V_3 \rightarrow V_4 \rightarrow V_5 \rightarrow V_6\]

也可以是:

\[V_1 \rightarrow V_2 \rightarrow V_3 \rightarrow V_4 \rightarrow V_6 \rightarrow V_5\]

BFS 实现(Kahn 算法)

重要

Kahn 算法主要基于节点的入读进行图遍历,每得到一个入读=0的节点,就得到下一个节点,具体操作如下:

1. 不断找寻有向图中入度为 0 的顶点,将其输出。
2. 然后删除入度为 0 的顶点和从该顶点出发的有向边。
3. 重复上述操作直到图为空,或者找不到入度为 0 的节点为止。

可以看到上述操作其是就是对图进行 BFS 遍历,其关键在于引入队列来暂存入度=0的节点,如果有环,就会发现环里面的节点入度都大于0,也就导致不能完美遍历整个图,也就是排序结果数小于总节点数。

 1// 使用 inDegree[i] 维护节点的入读
 2// 使用 queue 维护入读为 0 的节点,然后再以 queue 中的节点作为出发点,继续遍历下一层节点
 3class Solution {
 4    public int[] findOrder(int numCourses, int[][] prerequisites) {
 5        // 排序结果指针
 6        int resultIndex = 0;
 7        // 排序结果容器
 8        int[] result = new int[numCourses];
 9        // 维护每个节点的入度 inDegree[i]
10        int[] inDegree = new int[numCourses];
11        List<List<Integer>> edges = new ArrayList<>();
12        for (int i = 0; i < numCourses; i++) {
13            edges.add(new ArrayList<>());
14        }
15        for (int[] it : prerequisites) {
16            edges.get(it[1]).add(it[0]);
17            inDegree[it[0]]++;
18        }
19        Queue<Integer> queue = new ArrayDeque<>();
20        for (int i = 0; i < numCourses; i++) {
21            if (inDegree[i] == 0) {
22                // 入度为 0 的节点加入队列和最终结果
23                queue.add(i);
24                result[resultIndex++] = i;
25            }
26        }
27        while (!queue.isEmpty()) {
28            // queue 维护入读为 0 的节点, 删除入度为 0 的 vertex
29            Integer vertex = queue.poll();
30            // 访问 vertex 的下一层
31            for (Integer next : edges.get(vertex)) {
32                // 删除 vertex 后,相连的节点入读就需要减 1
33                inDegree[next]--;
34                if (inDegree[next] == 0) {
35                    // 入度为 0 的节点加入队列和最终结果
36                    queue.add(next);
37                    result[resultIndex++] = next;
38                }
39            }
40        }
41        // 这里很重要,只有排序结果数和节点数想等,才说明没有环,当前图存在一个拓扑排序
42        return resultIndex == numCourses ? result : new int[0];
43    }
44}

DFS 实现

重要

基于 DFS 实现拓扑排序算法的基本思想:

  1. 对于一个顶点 u,深度优先遍历从该顶点出发的有向边 <u,v>:

    • 如果从该顶点 u 出发的所有相邻顶点 v 都已经搜索完毕,则回溯到顶点 u 时,该顶点 u 应该位于其所有相邻顶点 v 的前面(拓扑序列中)。

  2. 这样一来,当我们对每个顶点进行深度优先搜索,在回溯到该顶点时将其放入栈中,则最终从栈顶到栈底的序列就是一种拓扑排序。

DFS 遍历的一个重点是一边遍历,一边使用 visited[] 标记节点状态(0-未访问 1-遍历中 2-遍历完毕),如果 有环,那么深度优先遍历从该顶点出发的有向边时,就会发现有节点已经被标记为 遍历中

 1class Solution {
 2    int resultIndex; // 配合 result 数组模拟栈
 3    int[] result; // 栈,保存拓扑排序结果
 4    int[] visited; // 用来标记访问状态(访问当前节点所有相邻节点) 0-未访问 1-访问中 2-访问完
 5    boolean valid = true; // 标记是否有环
 6
 7    public int[] findOrder(int numCourses, int[][] prerequisites) {
 8        resultIndex = numCourses - 1;
 9        result = new int[numCourses];
10        visited = new int[numCourses];
11        List<List<Integer>> edges = new ArrayList<>();
12        for (int i = 0; i < numCourses; i++) {
13            edges.add(new ArrayList<>());
14        }
15        for (int i = 0; i < prerequisites.length; i++) {
16            int[] it = prerequisites[i];
17            edges.get(it[1]).add(it[0]);
18        }
19        for (int i = 0; i < numCourses && valid; i++) {
20            if (visited[i] == 0) {
21                dfs2(edges, i);
22            }
23        }
24        return valid ? result : new int[0];
25    }
26
27    public void dfs2(List<List<Integer>> edges, int vertex) {
28        // 将当前节点标记为 访问中
29        visited[vertex] = 1;
30        // 深度优先遍历从该顶点出发的有向边
31        for (Integer next : edges.get(vertex)) {
32            if (visited[next] == 0) {
33                dfs2(edges, next);
34            } else if (visited[next] == 1) {
35                // cycle
36                valid = false;
37                return;
38            }
39        }
40        // 从该顶点 vertex 出发的所有相邻顶点都已经搜索完毕
41        result[resultIndex--] = vertex;
42        // 将当前节点标记为 访问完
43        visited[vertex] = 2;
44    }
45}

参考

  1. 拓扑排序简介. 算法通关手册(LeetCode)