拓扑排序详解

本文通过课程表II的问题介绍拓扑排序的概念,展示了如何使用Java实现拓扑排序。文章首先解释了拓扑排序的定义,然后详细解析了一道课程表问题的解题过程,包括构建有向图、遍历图以及判断环的存在。最后,提供了两种不同的解决方案——深度优先搜索(DFS)和广度优先搜索(BFS)。文章强调BFS解法相对更易理解。
摘要由CSDN通过智能技术生成

拓扑排序

参考文章:
拓扑排序,YYDS
Java 简单好理解的拓扑排序

1. 简介

对一个有向无环图(Directed Acyclic Graph简称DAG) G 进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。如图所示

拓扑排序

接下来通过一道题 课程表II ,来理解拓扑排序的应用场景和使用方法。

2. 课程表II

  • 题目描述
现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,
其中 prerequisites[i] = [a, b] ,表示在选修课程 a 前 必须 先选修 b 。
    例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。
如果不可能完成所有课程,返回 一个空数组 。
  • 实例
输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
输出:[0,2,1,3]
解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
  • 解题思路

可以把每一个课程看成一个点,prerequisites[i] 看成一条边。那么它们就是一副图。给出学习顺序,是不是就是将所有的课程排成一个线性序列。因此,可以判断这道题就是用拓扑排序来解决的。

  1. 首先需要根据prerequisites构建一个图(一般是用邻接表形式来构建图)
List<Integer>[] buildGraph(int numCourses, int[][] prerequisites) {
    //graph[i]存储着节点i所指向的节点
    List<Integer>[] graph = new LinkedList[numCourses];
    for (int i = 0; i < numCourses; i++) {
        graph[i] = new LinkedList<>();
    }
    for (int[] edge : prerequisites) {
        int from = edge[1];
        int to = edge[0];
        // 修完课程 from 才能修课程 to
        // 在图中添加一条从 from 指向 to 的有向边
        graph[from].add(to);
    }
    return graph;
}
  1. 遍历图
// 防止重复遍历同一个节点
boolean[] visited;
// 从节点 s 开始 BFS 遍历,将遍历过的节点标记为 true
void traverse(List<Integer>[] graph, int s) {
    //如果节点已经访问过,直接返回
    if (visited[s]) {
        return;
    }
    /* 前序遍历代码位置 */
    // 将当前节点标记为已遍历
    visited[s] = true;
    for (int t : graph[s]) {
        traverse(graph, t);
    }
    /* 后序遍历代码位置 */
}
  1. 判断图中有没有环
//记录当前traverse经过的路径
boolean[] onPath;
//是否有环
boolean hasCycle = false;
//记录当前节点是否访问过
boolean[] visited;
//遍历图
void traverse(List<Integer>[] graph, int s) {
    //如果节点又一次被访问到,说明有环
    if (onPath[s]) {
        hasCycle = true;
    }
    if (visited[s]) {
        return;
    }
    // 将节点 s 标记为已遍历
    visited[s] = true;
    // 开始遍历节点 s
    onPath[s] = true;
    for (int t : graph[s]) {
        traverse(graph, t);
    }
    // 节点 s 遍历完成
    onPath[s] = false;
}

所有的准备工作都完成了,接下来直接给出答案。

DFS题解
//记录遍历过的节点,防止重复遍历同一个节点
boolean[] visited;
//记录一次traverse递归经过的节点
boolean[] onPath;
//记录图中是否有环
boolean hasCycle = false;
//用来记录课程顺序
List<Integer> postorder = new ArrayList<>();

public int[] findOrder(int numCourses, int[][] prerequisites) {
    //构建图
    List<Integer>[] graph = buildGraph(numCourses, prerequisites);
    
    visited = new boolean[numCourses];
    onPath = new boolean[numCourses];
    
    //因为图中不是所有节点都相连,所以要遍历每一个节点
    for (int i = 0; i < numCourses; i++) {
        traverse(graph,i);
    }
    //如果有环,就不能实现拓扑排序,直接返回
    if (hasCycle)
        return new int[]{};
    Collections.reverse(postorder);
    //将postorder转换成int数组,stream是 java 8新特性,不熟悉的可以查一下
    return postorder.stream().mapToInt(Integer::intValue).toArray();
}
//构建图
private List<Integer>[] buildGraph(int numCourses, int[][] prerequisites) {
    //使用邻接表的方式来构建图
    List<Integer>[] graph = new ArrayList[numCourses];
    for (int i = 0; i < numCourses; i++) {
        graph[i] = new ArrayList<>();
    }
    for (int[] prerequisite : prerequisites) {
        int from = prerequisite[1];
        int to = prerequisite[0];
        graph[from].add(to);
    }
    return graph;
}
//遍历图
public void traverse(List<Integer>[] graph,int s){
    if (onPath[s])
        hasCycle = true;
    if (visited[s] || hasCycle)
        return;
    //将当前节点标记为已遍历
    visited[s] = true;
    //开始遍历节点
    onPath[s] = true;
    for (Integer integer : graph[s]) {
        traverse(graph,integer);
    }
    //节点遍历结束
    onPath[s] = false;
    //后续遍历位置,不理解这里的。可以去看文章开头的参考文章。里边有详细讲解
    postorder.add(s);
  }
}
BFS题解

思路

  1. 建立入度表,入度为 0 的节点先入队
  2. 当队列不为空,节点出队,标记学完课程数量的变量加 1,并记录该课程
  3. 将课程的邻居入度减 1
  4. 若邻居课程入度为 0,加入队列
public int[] findOrder(int numCourses, int[][] prerequisites) {
    if (numCourses == 0) 
        return new int[0];
    //建立邻接表
    List<Integer>[] graph = new LinkedList[numCourses];
    for (int i = 0; i < numCourses; i++) {
        graph[i] = new LinkedList<>();
    }
    //建立入度表,即有几个节点指向此节点
    int[] inDegrees = new int[numCourses];
    for (int[] p : prerequisites) { 
        int from = p[1];
        int to = p[0];
        //添加一条from到to的有向边
        graph[from].add(to);
        //p[1]指向p[0],所以p[0]的入度加一
        inDegrees[to]++;
    }
    // 将入度为0的节点加入队列中
    Queue<Integer> queue = new LinkedList<>();
    for (int i = 0; i < inDegrees.length; i++) {
        if (inDegrees[i] == 0) 
            queue.offer(i);
    }
    //记录学完的课程数量
    int count = 0; 
    //记录学完的课程
    int[] res = new int[numCourses];  
    //根据提供的先修课列表,删除入度为 0 的节点
    while (!queue.isEmpty()){
        int curr = queue.poll();
        //将可以学完的课程加入结果当中
        res[count++] = curr; 
        //获得学完课程的相邻节点
        List<Integer> part = graph[curr];
        for (int p : part) {
           //入度减 1 
           inDegrees[p]--;
           //如果入度为0,就可以学习该课程,加入队列
           if (inDegrees[p] == 0) 
               queue.offer(p);
          }
        }
    if (count == numCourses) 
        return res;
    //如果count != numCourses,说明存在环,不能修完所有课程
    return new int[0];
    }

PS
个人觉得 bfs题解更容易理解,建议掌握。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值