数据结构与算法之拓扑排序

在这里插入图片描述

数据结构与算法系列

数据结构与算法之哈希表

数据结构与算法之跳跃表

数据结构与算法之字典树

数据结构与算法之2-3树

数据结构与算法之平衡二叉树

数据结构与算法之十大经典排序

数据结构与算法之二分查找三模板

数据结构与算法之动态规划

数据结构与算法之回溯算法

数据结构与算法之Morris算法

数据结构与算法之贪心算法

数据结构与算法之拓扑排序

数据结构与算法之拓扑排序

前言

今天忽然想起了去年华为软挑的题目,与图论有关,其中也涉及到了一部分拓扑排序的知识,然后本来想写一些BFS和DFS总结的一些套路,忽然觉得不如直接拓扑排序讲解一下,也是甚好,那么今天就介绍一下拓扑排序吧。

定义

以下是拓扑排序在维基百科上的定义。

拓扑排序(Topological sorting)
在计算机科学领域,有向图的拓扑排序是对其顶点的一种线性排序,使得对于从顶点u到顶点v的每个有向边uv,u在排序中都在v之前。
例如,图形的顶点可以表示要执行的任务(Activity),并且边可以表示一个任务必须在另一个任务之前执行的约束;在这个应用中,拓扑排序只是一个有效的任务顺序。
当且仅当图中没有定向环时(即有向无环图),才有可能进行拓扑排序。
任何有向无环图至少有一个拓扑排序。已知有算法可以在线性时间内,构建任何有向无环图的拓扑排序。

从拓扑排序的定义可以知晓,这一算法的作用是两个,第一,用来梳理图顶点的指向顺序;第二,可以用来判断图是否为有向无环图(DAG,Directed Acyclic Graph)。此外,拓扑排序并不是一个排序算法,而是给出一个任务执行的线性顺序(不唯一)。

名词解释

针对前文定义以及后文需要,这里加入一些图论中的名词解释。

  1. 前驱活动:拓扑排序中的定义提到有向边用来表示任务之间的约束,而前驱活动即为要执行某一任务的前驱任务。
  2. 入度:顶点p的入度指的是有其余顶点指向顶点p的边总数
  3. 出度:顶点p的出度指的是顶点p指向其余顶点的边总数
  4. 有向无环图:在图论中,如果一个有向图从任意顶点出发无法经过若干条边回到该点,则这个图是一个有向无环图(DAG)。

算法流程

拓扑排序有两种算法实现。

卡恩算法

卡恩算法的步骤如下:

  1. 假设L为最后结果集合,先找到入度为0的顶点放入到L中,去除掉这些顶点的边(出度)
  2. 重复步骤1直到找不出入度为0的顶点。
  3. 比较L中顶点个数与图中节点总数是否一致。
    这么一看,卡恩算法其实就是BFS的操作先遍历所有出度为0的顶点,然后遍历出度为0的所有出度指向的顶点,以此类推。

2. DFS(深度优先搜索)

深度优先搜索算法就比较随意了,其实应该称作为DFS+回溯算法。
从一个顶点开始,沿着顶点出度的某一路径进行深度优先搜索访问

  1. 一直访问到到出度为0的顶点后往前回溯,继续其他出度顶点的访问。
  2. 如果遇到顶点出度指向是路径中已经包含的顶点,那么说明有环,无法进行拓扑排序。

下面就举一个典型例子来进行讲解。
在这里插入图片描述

拓扑排序例题

课程表II

课程表II是剑指offer中选出的比较有特殊的贪心算法的题目。

问题描述

力扣
现在你总共有 n 门课需要选,记为 0 到 n-1。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。
示例

问题分析

题目是典型的拓扑排序,题目意思很明显了,就是想让我们使用BFS(卡恩算法去解决),这里我们还是分别给出两种算法的代码,供大家去参考一下。
二者共同的地方都需要先建立我们的有向图(存储我们的有向图)。
随后,
卡恩算法需要关注入度为0顶点的出度。
DFS算法需要注意深度搜素时顶点状态的变化。

卡恩算法

class Solution {
    // 存储有向图
    List<List<Integer>> edges;
    // 存储每个节点的入度
    int[] indegree;
    // 存储答案
    int[] result;
    int index;
    public int[] findOrder(int numCourses, int[][] prerequisites) {
    	//建图
        edges = new ArrayList<List<Integer>>();
        for (int i = 0; i < numCourses; ++i) {
            edges.add(new ArrayList<Integer>());
        }
        //初始化入度和结果数组
        indegree = new int[numCourses];
        result = new int[numCourses];
        for (int[] vertex : prerequisites) {
            edges.get(vertex[1]).add(vertex[0]);
            indegree[vertex[0]]++;
        }
        
		//BFS使用队列形式实现
        Queue<Integer> queue = new LinkedList<Integer>();
        // 将所有入度为 0 的节点放入队列中
        for (int i = 0; i < numCourses; ++i) {
            if (indegree[i] == 0) {
                queue.offer(i);
            }
        }
        index = 0;
        while (!queue.isEmpty()) {
            // 逐个取出入度为0的节点u
            int u = queue.poll();
            // 放入答案中
            result[index] = u;
            index++;
            for (int v: edges.get(u)) {//取出度指向顶点v
                indegree[v]--;
                // 再去除掉u->v的边之后,如果相邻节点v的入度为0,同样加入到队列中
                if (indegree[v] == 0) {
                    queue.offer(v);
                }
            }
        }

        if (index != numCourses) {
            return new int[0];
        }
        return result;
    }
}
DFS代码

DFS算法相对简单,就直接使用力扣官方的答案给大家咯。


//作者:LeetCode-Solution
//链接:https://leetcode-cn.com/problems/course-schedule-ii/solution/ke-cheng-biao-ii-by-leetcode-solution/
class Solution {
    // 存储有向图
    List<List<Integer>> edges;
    // 标记每个节点的状态:0=未搜索,1=搜索中,2=已完成
    int[] visited;
    // 用数组来模拟栈,下标 n-1 为栈底,0 为栈顶
    int[] result;
    // 判断有向图中是否有环
    boolean valid = true;
    // 栈下标
    int index;

    public int[] findOrder(int numCourses, int[][] prerequisites) {
        edges = new ArrayList<List<Integer>>();
        for (int i = 0; i < numCourses; ++i) {
            edges.add(new ArrayList<Integer>());
        }
        visited = new int[numCourses];
        result = new int[numCourses];
        index = numCourses - 1;
        for (int[] info : prerequisites) {
            edges.get(info[1]).add(info[0]);
        }
        // 每次挑选一个「未搜索」的节点,开始进行深度优先搜索
        for (int i = 0; i < numCourses && valid; ++i) {
            if (visited[i] == 0) {
                dfs(i);
            }
        }
        if (!valid) {
            return new int[0];
        }
        // 如果没有环,那么就有拓扑排序
        return result;
    }

    public void dfs(int u) {
        // 将节点标记为「搜索中」
        visited[u] = 1;
        // 搜索其相邻节点
        // 只要发现有环,立刻停止搜索
        for (int v: edges.get(u)) {
            // 如果「未搜索」那么搜索相邻节点
            if (visited[v] == 0) {
                dfs(v);
                if (!valid) {
                    return;
                }
            }
            // 如果「搜索中」说明找到了环
            else if (visited[v] == 1) {
                valid = false;
                return;
            }
        }
        // 将节点标记为「已完成」
        visited[u] = 2;
        // 将节点入栈
        result[index--] = u;
    }
}

总结

拓扑排序是图论中进行有向无环图判断的最常用的方法。而实现拓扑排序又有着两种实现方法,卡恩算法(BFS)和DFS算法。在拓扑排序中,两种算法都有着自己的注意点。卡恩算法需要注意取出入度为0顶点之后,这些顶点出度的删除;DFS算法则需要在回溯和路径深入过程中访问顶点的状态变化。

如有兴趣,可以关注我的公众号,每周和你一起修炼数据结构与算法。
在这里插入图片描述

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT 涓涓清泉

感谢打赏,我会更加努力写更好的

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值