![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/6998d916341a0b0c4b5a3a5e58fa8231.png)
这是图的拓扑排序相关的经典问题。如果没有学过图的拓扑排序确实不好做。我们只需要将其抽象为有向图,然后判断是否可以完成拓扑排序即可。
深度优先遍历1(逆拓扑排序)
解题思路:时间复杂度O(
m
+
n
m+n
m+n)n是课程数,m是n的前驱个数,空间复杂度O(
n
+
m
n+m
n+m)我们需要存储其抽象为图数据结构的邻接表 |
---|
深度优先遍历实现拓扑排序比较绕,但是为什么将深度优先放在前面,因为深度优先遍历是面试最喜欢考的(逆向思维)。因为广度优先遍历的逻辑和我们人脑一样,而深度优先却将过程反了过来,很适合考察面试者的逻辑处理能力。
- 拓扑排序,就是图满足绝对先后关系。
- 也就是每次输出的结点,必须是入度为0的结点。
- 例如下图:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/d4e6a1076deb168095cdf69f53f5af69.png)
- B和D的入度为0,所以先输出他俩,D和B。然后剩余A的入度为0
- 输出A后,C和F的入度为0
- 输出C和F后,E和G入度为0
- 最后E和G输出后,所有结点输出完成
- 如果发现还有结点没有输出,但是没有入度为0的结点时,此时表明这个图,不能完成拓扑排序
- 拓扑排序结果不唯一,例如BACDFEG也是结果之一。
但是以上过程是我们人脑的思路,深度优先遍历很难轻松完成以上过程的复现。所以我们得反过来思考。
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/11117584794147b682cae886af45ab28.png)
- 构建邻接表,统计每个结点的出度边(当前顶点指向哪些顶点)
- 然后依次深度优先遍历
- 访问A,发现它指向C和E和F,访问F,发现F指向G,访问G,发现G不指向任何顶点,输出G(如果想要正着输出,需要放入栈中)
- 回到F,发现F还指向E,访问E,发现E没有指向任何顶点,输出E
- 回到F,F指向都已经输出,输出F
- 回到A,发现A还指向C,C没有指向,输出C
- 回到A,A的指向都被输出,输出A
- A已经深度遍历完成,接下来对下一个没有输出的B进行,B指向A和C都已经输出,则输出B
- 下一个没有输出的是D,对D进行深度优先遍历,发现D指向F,但是F已经输出,则输出D
- 故我们获得了GEFCABD的逆拓扑排序结构,如果想要非逆拓扑排序,可以使用法二的思路(及其的难,只有高精度导航系统等需要用,实战中的代码也会及其复杂,法二中我只给出关键逻辑),或者用栈,依次入栈后出栈,就完成了逆拓扑排序的正序输出
深度优先遍历2(我当时华为三面的算法题)
是大厂(华为三面,技术面)中的一道面试题,要求使用深度优先遍历,优先处理入度为0的顶点。这个算法的使用场景是人工智能导航系统的算法,如果先处理入度不为0的顶点,后面再处理入度为0的顶点时,会数据不一致。但是又不能使用广度优先(需要用大量的锁,导致无法及时处理信息)
解题思路:时间复杂度O(
m
+
n
m+n
m+n)n是课程数,m是n的前驱个数,空间复杂度O(
n
+
m
n+m
n+m)我们需要存储其抽象为图数据结构的邻接表 |
---|
- 法一中,我们最后需要将栈中元素倒着输出才是拓扑排序结果
- 有没有办法还是先处理入度为0的顶点呢?
- 不可以使用队列,因为队列需要编程语言自己处理,而我们需要原子操作,也就是系统底层的栈帧来操作,可以省下很多锁操作。而且底层的栈非常快,编程语言的队列没有系统底层的栈帧快。
- 因此,用邻接表就无法完成此操作了。我们不再统计每个结点的出度(每个结点伸出去的边),而是再次逆转思想(逆中逆),选择统计每个结点的入度(有哪些结点直接指向当前顶点)
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/d6d1739d3268a4149e5bd2922ed0005c.png)
- 我们创建一个容器(数组,栈,链表都可以),这个容器每个元素都保存一个链表
- 这个链表表示单个顶点的所有前驱(有几条边指向它)。例如corses[1] = node{val = 3,next = node{val = 2,next = node{val = 0,next = null}}};表示对于课程1,有3,2,0号课程指向它,学习1,必须先完成3,2,0
- 结点A,有B指向它
- 结点B,无结点指向
- 结点C,有A和B指向它
- 结点D,无结点指向
- 结点E,有A和F指向它
- 结点F,有A和D指向它
- 结点G,有F指向它
- 构建完成后,我们依次深度优先遍历结点。每访问一个结点,将结点标识为true
- 先访问结点A,我们发现B指向它,访问B,然后发现B无结点指向(入度为0)
- 输出B,标识B已经输出,然后回到A,输出A,标识A已经输出
- 然后访问C,发现A和B指向它,但是A和B已经输出,所以输出C,标识C已经输出
- 访问D,无结点指向,直接输出
- 访问E,A和F指向,但是A已经输出,直接访问F。发现F有A和D指向,但是A和D已经标识为输出。直接输出F。然后回到E,输出E
- 访问F,发现已经输出,直接跳过
- 访问G,发现F指向,但是F已经输出,直接输出G
- 我们发现全程完整输出,这是一个可以拓扑排序的图。但是如果在深度优先遍历过程中,发现没有输出,但是已经访问过的结点,这就说明图中有环,深度优先遍历兜了一圈又回来了。出现这种情况直接返回false即可
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/80aa019d77b4cde4148d73373cc68807.png)
class Solution {
static class Node {
Node next;
int val;
public Node(){
this.val = -1;
}
public Node(int val){
this.val = val;
}
public Node(int val,Node next){
this.val = val;
this.next = next;
}
}
public boolean canFinish(int numCourses, int[][] prerequisites) {
Node[] corses = new Node[numCourses];
for (int[] pre : prerequisites) {
corses[pre[0]] = new Node(pre[1],corses[pre[0]]);
}
boolean[] finished = new boolean[numCourses];
boolean[] visited = new boolean[numCourses];
for (int i = 0; i < numCourses; i++) {
if (!dfs(corses, finished, visited, i)) {
return false;
}
}
return true;
}
private boolean dfs(Node[] corses, boolean[] finished, boolean[] visited, int i) {
if (finished[i]) return true;
if (visited[i]) return false;
visited[i] = true;
Node node = corses[i];
while (node != null) {
if (!dfs(corses, finished, visited, node.val)) return false;
node = node.next;
}
finished[i] = true;
return true;
}
}
广度优先
解题思路:时间复杂度O(
m
+
n
m+n
m+n)n是课程数,m是n的前驱个数,空间复杂度O(
n
+
m
n+m
n+m)我们需要存储其抽象为图数据结构的邻接表 |
---|
- 将其抽象为邻接表(一个数组,下标表示每个顶点,数组元素保存一个链表,链表表示每个顶点的边指向哪里)
- 我们初始化邻接表的过程中,要统计每个顶点的入度
- 然后开始依次遍历入度为0的结点u
- 如果u遍历完成后,它的邻居的入度变为0,则将邻居加入队列下次遍历
- 期间统计访问的顶点个数,如果最后正好将图遍历完,就说明是拓扑排序返回true,否则返回false
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/65bef82d06eb6a7b565eaf7f8cd6ad0e.png)
class Solution {
List[] edges;
int[] indeg;
public boolean canFinish(int numCourses, int[][] prerequisites) {
edges = new List[numCourses];
for (int i = 0; i < numCourses; ++i) {
edges[i] = new ArrayList<Integer>();
}
indeg = new int[numCourses];
for (int[] info : prerequisites) {
edges[info[1]].add(info[0]);
++indeg[info[0]];
}
int[] queueA = new int[numCourses];
int queueHead = 0;
int queueRear = 0;
for (int i = 0; i < numCourses; ++i) {
if (indeg[i] == 0) {
queueA[queueRear++] = i;
}
}
int visited = 0;
while (queueHead<queueRear) {
++visited;
int u = queueA[queueHead++];
for (Object x: edges[u]) {
int v = (int) x;
--indeg[v];
if (indeg[v] == 0) {
queueA[queueRear++] = v;
}
}
}
return visited == numCourses;
}
}