如题:
-
解读题意:题目中的
numCourses
表示需要修读的课程总数,且课程编号是从0-numCourses-1
的,prerequisites
表示每两门课之间的依赖关系。我们需要给出各门课程学习的先后顺序,保证合理完成全部课程。如果无法完成所有课程,就返回空数组。 -
什么时候无法完成?
- 当课程之间存在循环依赖的时候就无法完成,例如完成
[1,2],[2,1]
这样,因此我们可以将这些依赖关系抽象成一个有向图,需要做的就是检测图中有没有环。
- 当课程之间存在循环依赖的时候就无法完成,例如完成
-
如果可以完成,如何记录下来先后顺序?
-
将所有课程依赖关系看成图,那么完成课程的顺序就是这个有向图的拓扑排序顺序,而拓扑排序就是一次遍历每个入度为0的结点,入度为0即不被任何结点指向。
-
有向图的拓扑排序可以利用
DFS
深度优先搜索或者BFS
广度优先搜索进行,这里我们是用DFS
:-
对于
DFS
来说,图的拓扑排序可以理解为后序遍历的结果然后反转。因为后序遍历先遍历叶子节点,叶子节点其实就是出度为0的结点,否则就不是叶子,也就是说一次遍历出度为翻0的所有借点,反转后就是根据入度为0的顺序遍历了。如下:public void traverse(List<Integer>[] graph,int s){ for (Integer child : graph[s]) { traverse(graph,child); } arrayList.add(s);//在遍历代码后的位置将当前结点加入集合中,这里就是后序遍历 }
-
注意:不能是先序遍历,因为有可能一个父母指向同一个孩子,此时先序遍历会直接遍历一个父母后就遍历孩子,此时该孩子还被另一个父结点指向,入度为1,遍历错误。
-
-
-
代码如下:
class Solution { boolean[] visited;//用来记录遍历过的结点 boolean Circle = false;//如果有环就将它置为true boolean[] onPath;//用来判断是否存在环 ArrayList<Integer> arrayList;//用来记录后序遍历的每个结点 public int[] findOrder(int numCourses, int[][] prerequisites) { int[] res = new int[numCourses];//用来返回最后的结果 arrayList = new ArrayList<>(numCourses); visited = new boolean[numCourses]; //visited数组此处不用来判断是否存在环,而是因为我们无法保证图联通,需要将所有节点依次作为 // 开始结点遍历图,此时如果依次遍历abc,下一次以b开始又会遍历bc,这就出现了重复遍历 //visited数组可以避免重复遍历的问题。 onPath = new boolean[numCourses]; //因此使用onPath记录 当前 走过的路径,每次遍历到新结点时判断onPath是否为true,如果是 //就代表之前遍历过这个结点,因此存在环,然后将Circle置为true。 List<Integer>[] graph = buildGraph(numCourses, prerequisites); for (int i = 0; i < numCourses; i++) { traverse(graph,i); } if (Circle)return new int[]{}; Collections.reverse(arrayList); for (int i = 0; i < arrayList.size(); i++) { res[i] = arrayList.get(i); } return res; } //建图,使用链表法,每个结点作为开始结点存在在List数组中,后面依次连接着它指向的结点 public List<Integer>[] buildGraph(int numCourses,int[][] prerequisites){ ArrayList<Integer>[] res = new ArrayList[numCourses]; for (int i = 0; i < res.length; i++) { res[i] = new ArrayList<>(); } for (int[] p : prerequisites){ res[p[1]].add(p[0]); //由于学习p[0]之前需要学习p[1],因此由p[1]指向p[0] } return res; } //遍历图判断是否有环,并且记录后序遍历的结点 public void traverse(List<Integer>[] graph,int s){ if (onPath[s]){ Circle = true; //不是return,而是用全局变量记录,因为可能只是一个分支有环,return只是让当前分支不再继续, //此时我们应该让其它分支也不用再继续遍历。 //要注意,我们的onPath数组需要在退出的时候清除标记,即只记录当前路径,因为如果和visited // 一样的话,就无法判断是有环而重复还是从子节点开始遍历而重复了,因为我们无法判断图是否连通, // 需要将所有结点遍历,如果遍历到了子节点那么也是ture,因此onPath只记录当前遍历的路径。 //注意:这里必须将判断环写在头而不是下面的visited写在头,因为我们只是让visited起到避免 //重复遍历子节点的问题,但他仍然是可以在一次遍历中检测环的,这是它的本职工作,我们只是不用罢了。 //如果将visited放在头部,那么碰到环还是可以被他检测出来,那么它无法分辨到底是重复遍历子节点导致 //条件成立还是因为环导致条件成立。 } if(visited[s] || Circle){ return; } visited[s] = true; onPath[s] = true; for (Integer child : graph[s]) { traverse(graph,child); } arrayList.add(s);//将当前结点加入集合中 onPath[s] = false; } }