一天一个经典算法:拓扑排序算法(春节档)

378 阅读3分钟

「这是我参与2022首次更文挑战的第16天,活动详情查看:2022首次更文挑战」。

有向图的拓扑排序或拓扑定序是对其顶点的一种线性排序,使得对于从顶点 u 到顶点 v 的每个有向边 uv, u 在排序中都在 v 之前。

例如,图形的顶点可以表示要执行的任务,并且边可以表示一个任务必须在另一个任务之前执行的约束;在这个应用中,拓扑排序只是一个有效的任务顺序。

当且仅当图中没有定向环时(【有向无环图】),才有可能进行拓扑排序。

任何有向无环图至少有一个拓扑排序。已知有算法可以在线性时间内,构建任何有向无环图的拓扑排序。

在【图论】中,由一个有向无环图的顶点组成的序列,满足下列条件时,才能称为该图的一个拓扑排序:

  1. 序列中包含每个顶点,且每个顶点只出现一次;
  2. 若A在序列中排在B的前面,则在图中不存在从B到A的路径。

案例

image.png

上图有许多有效的顶点拓扑排序,例如,

7 5 3 1 4 2 0 6
7 5 1 2 3 4 0 6
5 7 3 1 0 2 6 4
3 5 7 0 1 2 6 4
5 7 3 0 1 4 6 2
7 5 1 3 4 0 6 2
5 7 1 2 3 0 6 4
3 7 0 5 1 4 2 6
。。。

注意,对于每个有向边u —> vuv排序中都排在前面。例如,拓扑顺序的图形表示[7, 5, 3, 1, 4, 2, 0, 6]为:

image.png

拓扑排序算法有两种,【卡恩算法】和【深度优先搜索】。

这篇文章我们先介绍【卡恩算法】。简单来说,假设L是存放结果的列表,先找到那些入度为零的节点,把这些节点放到L中,因为这些节点没有任何的父节点。

然后把与这些节点相连的边从图中去掉,再寻找图中的入度为零的节点。对于新找到的这些入度为零的节点来说,他们的父节点已经都在L中了,所以也可以放入L。重复上述操作,直到找不到入度为零的节点。

如果此时L中的元素个数和节点总数相同,说明排序完成;

如果L中的元素个数和节点总数不同,说明原图中存在环,无法进行拓扑排序。

Java示例

image.png

// 用索引对图进行排序

import java.util.*;
 
// 图
class Graph {
    // 顶点数
    int V;
 
    // 列表数组,其中包含对每个顶点的邻接表的引用
    List<Integer> adj[];
    // 构造函数
    public Graph(int V)
    {
        this.V = V;
        adj = new ArrayList[V];
        for (int i = 0; i < V; i++)
            adj[i] = new ArrayList<Integer>();
    }
 
    // 向图形添加边
    public void addEdge(int u, int v)
    {
        adj[u].add(v);
    }
    // 打印完整图的拓扑排序
    public void topologicalSort()
    {
        // 创建一个数组以存储所有顶点的indegree。将所有indegree初始化为 0。
        int indegree[] = new int[V];
 
        // 遍历邻接列表以填充顶点的索引
        for (int i = 0; i < V; i++) {
            ArrayList<Integer> temp
                = (ArrayList<Integer>)adj[i];
            for (int node : temp) {
                indegree[node]++;
            }
        }
 
        // 创建一个队列,并使用indegree 0将所有顶点排序
        Queue<Integer> q = new LinkedList<Integer>();
        for (int i = 0; i < V; i++) {
            if (indegree[i] == 0)
                q.add(i);
        }
 
        // 初始化访问顶点的计数
        int cnt = 0;
 
        // 创建一个向量来存储结果
        Vector<Integer> topOrder = new Vector<Integer>();
        while (!q.isEmpty()) {
            int u = q.poll();
            topOrder.add(u);
 
            // 遍历其所有相邻的节点u,并将其次数减少1
            for (int node : adj[u]) {
                // 如果 in-degree 变为0,天假到队列中
                if (--indegree[node] == 0)
                    q.add(node);
            }
            cnt++;
        }
 
        // 检查是否有循环
        if (cnt != V) {
            System.out.println(
                "图中有循环");
            return;
        }
 
        // 打印拓扑顺序
        for (int i : topOrder) {
            System.out.print(i + " ");
        }
    }
}
class Main {
    public static void main(String args[])
    {
        // 创建上面的图
        Graph g = new Graph(6);
        g.addEdge(5, 2);
        g.addEdge(5, 0);
        g.addEdge(4, 0);
        g.addEdge(4, 1);
        g.addEdge(2, 3);
        g.addEdge(3, 1);
        g.topologicalSort();
    }
}

输出:4 5 2 0 3 1

Kahn 的拓扑排序算法的时间复杂度为O(V + E),其中VE分别是图中顶点和边的总数。队列需要存储图的所有顶点。所需的空间是 O(V)。