1. 什么是有向图
有向图(Directed Graph),也被称为有向图形或方向图,是一种图的类型。在有向图中,图中的边具有方向,从一个顶点指向另一个顶点。
在有向图中,每个顶点表示一个实体,而有向边则表示实体之间的关系或连接。这种有方向性的边表明了连接的起点和终点之间的单向关系。因此,有向图中的边具有起点和终点的概念,它们不能逆转方向。
与有向图对应的是无向图(Undirected Graph),在无向图中,边是没有方向的,可以双向移动。相比之下,有向图更适合描述具有明确方向性的关系,例如有向的路径、进程之间的依赖关系等。
有向图可以用来解决许多问题,如拓扑排序、最短路径、网络流等。它在计算机科学、图论、网络分析等领域都有广泛的应用。
2. 什么是拓扑排序
拓扑排序(Topological Sort)是对有向无环图(DAG)进行排序的一种算法
。它将有向图的所有顶点排列成线性序列,使得对于任何的有向边 (u, v),顶点 u 都在序列中排在顶点 v 的前面。
拓扑排序的应用场景通常涉及到任务或事件之间的依赖关系,其中每个顶点表示一个任务或事件,有向边表示依赖关系。通过拓扑排序,可以确定这些任务或事件的执行次序,以满足依赖关系的约束。
拓扑排序算法的实现过程如下:
- 找到没有前置依赖的顶点,即
入度为0的顶点
,并将其加入结果序列
中。 - 从
图中删除该顶点及其相关的边,即更新其他顶点的入度
。 - 重复步骤1和步骤2,直到图中所有顶点都被添加到结果序列中,或者
无法找到入度为0的顶点为止
。 - 如果图中存在环路,则无法进行拓扑排序(或者排序得到的数组集合不等于节点总数),因为环路意味着存在循环依赖。
拓扑排序可以通过深度优先搜索(DFS)或广度优先搜索(BFS)来实现。其中,DFS
算法更常用,它可以按照深度优先的顺序遍历图,并在遍历完成后逆序得到拓扑排序结果。
拓扑排序的时间复杂度为O(V+E),其中V和E分别是图中的顶点数和边数。
2. 有向图的拓扑排序
例如有向图:[[1,0],[2,0],[3,1],[4,3],[4,2],[5,4]]
数组后面的元素指向前面的元素
2. 1 BFS 广度优先
- 广度优先搜索需要依赖一个节点的入度数组如下:(当然这里节点也作为数组下标,如果节点和下标不能一一对应,可以使用一个map哈希表去记录下节点与入度的映射表)
- 然后还需要准备一个节点指向集合,记录节点与节点的指向关系(后续依照这个指向将被指向的节点的入度做减一操作)
- 最后就是准备一个队列,队列中依次加入入度为0 的节点,并且队列的出队顺序即为
- 再根据节点指向集合,若被指向的节点入度为0,那么此时把入度为0的节点加入到队列循环,直到没有入度为0 的节点
模板代码:
public int[] findOrder(int numCourses, int[][] prerequisites) {
int[] cou = new int[numCourses];//节点入度数组
int[] num = new int[numCourses];//用于存储拓扑排序
List<List<Integer>> couList = new ArrayList<>();//指向与被指向的集合映射
Queue<Integer> queue = new LinkedList<>();//辅助队列 用于处理入度为0 的节点
for(int i = 0 ;i<numCourses ;i++)//给集合中节点与被指向节点初始化集合
couList.add(new ArrayList<Integer>());
for(int[] pre : prerequisites){
cou[pre[0]]++;//统计各节点的入度
couList.get(pre[1]).add(pre[0]);//给集合中父节点设置指向子节点的子集合
}
for(int i = 0 ;i<numCourses ;i++){
if(cou[i] == 0) queue.offer(i);//搜索第一个入度为0 的节点 加入队列
}
int i = 0;//用于将拓扑排序加入到一个数组用的下标
while(!queue.isEmpty()){
int ids = queue.poll();
numCourses--;//取出一个元素 就让节点总数-1
num[i] = ids;//拓扑排序 取出的元素加入到数组
for(int cur : couList.get(ids)){// couList.get(ids) 根据节点 取出父节点指向的子节点 让被指向的子节点入度 -1
if(cou[cur] >= 1 ) cou[cur]--;
if(cou[cur] == 0 ) queue.offer(cur);//若当前节点入度为0 则加入队列
}
i++;
}
if(numCourses == 0) return num;
// 若 numCourses(节点总数) 不等于0说明 说明最后还有入度不为0的节点,没有被处理,说明有环,则无拓扑排序
// 或者如果拓扑排序数组num的长度 不等于节点总数 说明拓扑排序不完整,说明无拓扑排序
else return new int[0];
}
2. 2 DFS 深度优先
-
深度优先搜索需要依赖一个节点的辅助数组默认都为0如下:(当然这里节点也作为数组下标,如果节点和下标不能一一对应,可以使用一个map哈希表去记录下节点与辅助值的映射表)
标记值为0:代表搜索起点(dfs入口)
标记值为1:代表搜索中(如果搜索中碰到值标记值为1的节点,说明有环)
标记值为2:代表搜索完成(搜索过程无环) -
然后还需要准备一个节点指向集合,记录节点与节点的指向关系(后续依照这个指向将被指向的节点的值做标记)
-
最后当标记值为2的时候,就代表此次dfs无环,利用一个栈将标记值为2的节点加入到栈中(栈的加入顺序就是拓扑排序的顺序)
-
需要一个标志位(初始为true),如果搜索中dfs碰到节点值为1的节点的时候,代表出现了环,则直接将标志位标为false,结束此次递归
List<List<Integer>> cousList;
int[] cous;
boolean valid = true;
public boolean canFinish(int numCourses, int[][] prerequisites) {
cous = new int[numCourses];// 构造标志位 初始化全部位0 长度为节点数
cousList = new ArrayList<>();//给集合中节点与被指向节点初始化集合
for(int i = 0 ;i < numCourses ; i++) //指向(父节点)与被指向(子节点)的集合映射
cousList.add(new ArrayList<>());
for(int[] pre : prerequisites){//给集合中父1节点设置指向子节点的子集合
cousList.get(pre[1]).add(pre[0]);
}
for(int i = 0 ; i<numCourses ; i++){//
if(cous[i] == 0) dfs(i); //等于0未搜索过 进入dfs
}
return valid;
}
public void dfs(int c){
cous[c] = 1;
for(int cur : cousList.get(c)){//遍历该父节点的子节点集合
if(cous[cur]==0){//如果指向的节点未搜索过,则深搜
dfs(cur);
if(!valid){
return;
}
}else if(cous[cur]==1){//如果指向节点在搜索中,则有环,标记Vaild
valid = false;
return;
}
}
cous[c]=2;//因为节点已经完成深搜,所以标记它的状态搜索完成
}
// 方法二 dfs 深度优先
int[] cou = null;// 设置全局变量 方便dfs使用
int[] num = null;
List<List<Integer>> couList = null;
boolean valid = true;
Deque<Integer> queue = null;
public int[] findOrder(int numCourses, int[][] prerequisites) {
this.cou = new int[numCourses];// 构造标志位 初始化全部位0 长度为节点数
this.queue = new LinkedList<>();//用于配合输出拓扑排序
this.num = new int[numCourses];//用于存储拓扑排序
this.couList = new ArrayList<>();//给集合中节点与被指向节点初始化集合
for(int i = 0 ;i<numCourses ;i++)//指向(父节点)与被指向(子节点)的集合初始化
couList.add(new ArrayList<Integer>());
for(int[] pre : prerequisites){
couList.get(pre[1]).add(pre[0]);//指向(父节点)与被指向(子节点)的集合映射
}
for(int i = 0 ; i<numCourses ;i++){
if(cou[i] == 0) dfs(i);//等于0未搜索过 进入dfs
}
if(queue.size() != numCourses) return new int[0]; //如果dfs完成之后 栈内元素个数不等于节点总数 说明 拓扑排序不完整,存在环,自然不能将全部节点遍历完,
else{//否则就代表无环 可以得到完整的拓扑排序
for(int i = 0 ; i<numCourses ; i++){
num[i] = queue.pop();//将压栈的节点取出来 放到数组里面
}
}
return num;
}
public void dfs(int i){
cou[i] = 1;
for(int cur : couList.get(i)){//遍历该父节点的子节点集合
if(cou[cur] == 0){//节点标记数组对应的值等于 0 继续递归
dfs(cur);
if(!valid) return ; //根据标记为判断是否有环 有环说明不能得到拓扑排序 直接返回 不往下面执行了
}else if(cou[cur] == 1){//如果搜索中存在环 将标志位设为fasle
valid = false;
return;
}
}
//一次遍历结束无环 就让该遍历元素位置的节点数组数值置为 2 代表以该点进行dfs 无环
cou[i] = 2;
queue.push(i); //让该dfs完的节点压栈 为什么要压栈 因为最后的拓扑排序,就是栈的出栈顺序
}
3. 有向图有环无环判定
具体怎么判定有环和无环:
BFS下:
本身这两种判断方式原理是一样的,如果节点都能根据箭头遍历得到,自然得到的拓扑排序数组的长度就是节点总数,否则拓扑排序数组的长度不等于节点总数,说明有节点并没有遍历到,说明存在了环。
- 第一种判断方式:如果最后得到的数组(拓扑排序)的长度不等于节点总数,则代表有环,使得bfs并没有按照箭头指向走完整个图,所以出现了环
- 第二种判断方式:每次让队列弹出一个入度为0的节点时,让节点总数减1,如果最后节点总数 == 0,说明无环,每个元素都被bfs到了
DFS下:
- 最后的栈里面存的就是遍历到的节点,如果最后dfs结束后的栈的大小不等于节点总数,说明有节点没有被遍历到,说明出现了环,否则如果栈的大小等于节点数,代表都遍历到了,该图无环
栈的出栈顺序就是拓扑排序的循序