对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
拓扑排序主要用来解决有向图中的依赖解析(dependency resolution)问题。
举例来说,如果我们将一系列需要运行的任务构成一个有向图,图中的有向边则代表某一任务必须在另一个任务之前完成这一限制。那么运用拓扑排序,我们就能得到满足执行顺序限制条件的一系列任务所需执行的先后顺序。当然也有可能图中并不存在这样一个拓扑顺序,这种情况下我们无法根据给定要求完成这一系列任务,这种情况称为循环依赖(circular dependency)。
例1:如下图所示为一个有向图:
可以得到两个不同的拓扑排序结果:[1, 2, 3, 4, 5]
和[1, 2, 3, 5, 4]
。
拓扑排序存在的前提:
当且仅当一个有向图为有向无环图(directed acyclic graph,或称DAG)时,才能得到对应于该图的拓扑排序。每一个有向无环图都至少存在一种拓扑排序。该论断可以利用反证法证明如下:
假设我们有一由到这n个结点构成的有向图,且图中这些结点构成一个环。这即是说对于所有
1≤i
,图中存在一条有向边从指向。同时还存在一条从指向的边。假设该图存在一个拓扑排序。 那么基于这样一个有向图,显然我们可以得知对于所有1≤i
与普通的广度优先遍历唯一的区别在于需要维护每一个节点对应的入度,并在遍历的每一层时选取入度为0的节点开始遍历(而普通的广度优先遍历则无此限制,可以从每一层任意一个节点开始遍历)。这个算法描述如下:
**由于只有入度为0的节点才会被放入队列,当存在环时,环上的节点将不会放入队列,因此不会出现在最终的拓扑排序中。**事实上,在基于广度优先搜索的拓扑排序中,可以根据最终拓扑排序输出列表的长度是否等于图的节点数,来判断输入图是否存在拓扑排序。
时间复杂度: ,其中n
为图中的结点数目,e
为图中的边的数目
空间复杂度: ,需要存储成邻接表,空间复杂度为,
leetcode题:
import java.util.LinkedList;
import java.util.Queue;
/**
* Description:leetcode-207. 课程表
*
* 图、拓扑排序
*
* @author wzq
* @version 1.0.0
* @date 2023/4/3
*/
public class Main207 {
/**
*
* 你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
* 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,
* 表示如果要学习课程 ai 则 必须 先学习课程 bi 。
*
* 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
* 请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
*
* 输入:numCourses = 2, prerequisites = [[1,0]]
* 输出:true
* 解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
*
* [[0,2],[1,2],[2,0]] false
*
*/
/**
* 邻接表,记录指向的节点
*/
LinkedList<Integer> graph[];
/**
* 记录节点的入度
*/
int[] inDegree;
/**
* bfs实现
* @param numCourses 节点数
* @param prerequisites 依赖关系
* @return
*/
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 初始化
graph = new LinkedList[numCourses];
inDegree = new int[numCourses];
for (int i = 0; i < numCourses; i++) {
graph[i] = new LinkedList<>();
}
// 先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1
int row = prerequisites.length;
for (int i = 0; i < row; i++) {
int x = prerequisites[i][0];
int y = prerequisites[i][1];
if (graph[y].contains(x)){
continue;
}
// x依赖y, y指向x
graph[y].add(x);
inDegree[x] ++;
}
// 入度0添加
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; ++i) {
if (inDegree[i] == 0) {
queue.offer(i);
}
}
// 拓扑排序遍历
int visited = 0;
while (!queue.isEmpty()){
visited ++;
Integer point = queue.poll();
// 指向的节点入度 - 1
LinkedList<Integer> edge = graph[point];
for (Integer nextPoint : edge) {
inDegree[nextPoint] --;
// 入度0添加到队列
if (inDegree[nextPoint] == 0){
queue.offer(nextPoint);
}
}
}
return visited == numCourses;
}
public static void main(String[] args) {
Main207 main207 = new Main207();
int[][] arr = {{1,0}, {1,2}, {0, 1}};
System.out.println(main207.canFinish(3, arr));
}
}
import java.util.LinkedList;
import java.util.Queue;
/**
* Description:leetcode-210. 课程表 II
*
* 图、拓扑排序
*
* @author wzq
* @version 1.0.0
* @date 2023/4/4
*/
public class Main210 {
/**
* 邻接表,记录指向的节点
*/
LinkedList<Integer> graph[];
/**
* 记录节点的入度
*/
int[] inDegree;
/**
* bfs
* @param numCourses 节点数
* @param prerequisites 边,依赖关系
* @return
*/
public int[] findOrder(int numCourses, int[][] prerequisites) {
int[] result = new int[numCourses];
// 初始化
graph = new LinkedList[numCourses];
inDegree = new int[numCourses];
for (int i = 0; i < numCourses; i++) {
graph[i] = new LinkedList<>();
}
// 先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1
int row = prerequisites.length;
for (int i = 0; i < row; i++) {
int x = prerequisites[i][0];
int y = prerequisites[i][1];
if (graph[y].contains(x)){
continue;
}
// x依赖y, y指向x
graph[y].add(x);
inDegree[x] ++;
}
// 入度0添加
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; ++i) {
if (inDegree[i] == 0) {
queue.offer(i);
}
}
// 拓扑排序遍历
int visited = 0;
while (!queue.isEmpty()){
Integer point = queue.poll();
// 指向的节点入度 - 1
LinkedList<Integer> edge = graph[point];
for (Integer nextPoint : edge) {
// 入度0添加到队列
if (--inDegree[nextPoint] == 0){
queue.offer(nextPoint);
}
}
result[visited ++] = point;
}
if (visited != numCourses){
return new int[0];
}
return result;
}
public static void main(String[] args) {
Main210 main210 = new Main210();
int[][] arr = {{1,0}};
System.out.println(main210.findOrder( 2, arr));
}
}
使用深度优先搜索实现拓扑排序的基本思想是:对于一个特定节点,如果该节点的所有相邻节点都已经搜索完成,则该节点也会变成已经搜索完成的节点,在拓扑排序中,该节点位于其所有相邻节点的前面。一个节点的相邻节点指的是从该节点出发通过一条有向边可以到达的节点。
由于拓扑排序的顺序和搜索完成的顺序相反,因此需要使用一个栈存储所有已经搜索完成的节点。深度优先搜索的过程中需要维护每个节点的状态,每个节点的状态可能有三种情况:
初始时,所有节点的状态都是「未访问」。
每一轮搜索时,任意选取一个「未访问」的节点 u,从节点 u 开始深度优先搜索。将节点 u 的状态更新为「访问中」,对于每个与节点 u 相邻的节点 v,判断节点 v 的状态,执行如下操作:
时间复杂度: ,其中n
为图中的结点数目,e
为图中的边的数目
空间复杂度: ,对图进行深度优先搜索,我们需要存储成邻接表的形式,空间复杂度为,深度优先搜索的过程中,我们需要最多 O(n)的栈空间(递归)进行深度优先搜索,因此总空间复杂度为
leetcode题:
import java.util.LinkedList;
import java.util.Queue;
/**
* Description:leetcode-207. 课程表
*
* 图、拓扑排序
*
* @author wzq
* @version 1.0.0
* @date 2023/4/3
*/
public class Main207 {
/**
*
* 你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
* 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,
* 表示如果要学习课程 ai 则 必须 先学习课程 bi 。
*
* 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
* 请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
*
* 输入:numCourses = 2, prerequisites = [[1,0]]
* 输出:true
* 解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
*
* [[0,2],[1,2],[2,0]] false
*
*/
/**
* 邻接表,记录指向的节点
*/
LinkedList<Integer> graph[];
/**
* 记录节点访问状态, 0未访问 1访问中 2已访问
*/
int[] visited;
/**
* 是否存在环
*/
boolean isCircle = false;
/**
* dfs实现
* @param numCourses 节点数
* @param prerequisites 依赖关系
* @return
*/
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 初始化
graph = new LinkedList[numCourses];
visited = new int[numCourses];
for (int i = 0; i < numCourses; i++) {
graph[i] = new LinkedList<>();
visited[i] = 0;
}
// 先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1
int row = prerequisites.length;
for (int i = 0; i < row; i++) {
int x = prerequisites[i][0];
int y = prerequisites[i][1];
if (graph[y].contains(x)){
continue;
}
// x依赖y, y指向x
graph[y].add(x);
}
// 未访问的节点一直递归
for (int i = 0; i < numCourses && !isCircle; i++) {
if (visited[i] == 0) {
dfs(i);
}
}
return !isCircle;
}
private void dfs(int point){
visited[point] = 1;
// 递归指向的相邻节点
for (int nextPoint: graph[point]) {
if (visited[nextPoint] == 0) {
// 未访问则继续向下递归
dfs(nextPoint);
if (isCircle) {
return;
}
} else if (visited[nextPoint] == 1) {
// 出现环
isCircle = true;
return;
}
}
// 已访问
visited[point] = 2;
}
public static void main(String[] args) {
Main207 main207 = new Main207();
int[][] arr = {{1,0}, {1,2}, {0, 1}};
System.out.println(main207.canFinish( 3, arr));
}
}
import java.util.LinkedList;
/**
* Description:leetcode-210. 课程表 II
*
* 图、拓扑排序
*
* @author wzq
* @version 1.0.0
* @date 2023/4/4
*/
public class Main210 {
/**
* 邻接表,记录指向的节点
*/
LinkedList<Integer> graph[];
/**
* 记录节点访问状态, 0未访问 1访问中 2已访问
*/
int[] visited;
/**
* 是否存在环
*/
boolean isCircle = false;
/**
* 拓扑排序集
*/
int[] result;
/**
* 拓扑排序集下标
*/
int index;
/**
* dfs
* @param numCourses 节点数
* @param prerequisites 边,依赖关系
* @return
*/
public int[] findOrder(int numCourses, int[][] prerequisites) {
result = new int[numCourses];
// 初始化
graph = new LinkedList[numCourses];
visited = new int[numCourses];
index = numCourses - 1;
for (int i = 0; i < numCourses; i++) {
graph[i] = new LinkedList<>();
}
// 先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1
int row = prerequisites.length;
for (int i = 0; i < row; i++) {
int x = prerequisites[i][0];
int y = prerequisites[i][1];
if (graph[y].contains(x)){
continue;
}
// x依赖y, y指向x
graph[y].add(x);
}
// 未访问的节点一直递归
for (int i = 0; i < numCourses && !isCircle; i++) {
if (visited[i] == 0){
dfs(i);
}
}
if (isCircle){
return new int[0];
}
return result;
}
private void dfs(int point){
visited[point] = 1;
// 递归指向的相邻节点
for (Integer nextPoint : graph[point]) {
if (visited[nextPoint] == 0){
// 未访问则继续向下递归
dfs(nextPoint);
if (isCircle) {
return;
}
continue;
}
if(visited[nextPoint] == 1){
// 出现环
isCircle = true;
return;
}
}
// 已访问,由于被依赖的节点先遍历,记录在尾部
result[index --] = point;
visited[point] = 2;
}
public static void main(String[] args) {
Main210 main210 = new Main210();
int[][] arr = {{1,0}};
System.out.println(main210.findOrder( 2, arr));
}
}