有向图 拓扑排序 文件依赖下的编译顺序该如何确定?
有2种常用方式
1.kahn算法
2.基于深度优先的逆后序
都需要有向图中无环,否则依赖关系的顺序可能产生问题
若有 文件 a.c b.c c.c d.c 他们之间的依赖关系是
a文件被b文件依赖,b文件被c文件依赖,b文件被d文件依赖
那么哪个文件被先编译? 被依赖的最多的那个文件(a或d)应该被先编译。 如何得到正确的编译顺序?
a.c -> b.c -> c.c
d.c ->
1.kahn
//拓扑排序 //无环有向图 是 拓扑排序的前提 //拓扑排序后,顶点所依赖的前驱节点必定都先出现再他前面(这种序列叫做 拓扑序列) //只要满足上述条件的排序输出,都是拓扑序列(所以一个图往往会有多个拓扑序)(这种排序过程叫做 拓扑排序) // //拓扑排序再生活中的应用,比如任务的依赖关系,被依赖的基层任务应该先完成,比如穿几件衣服,一定是先把穿在里面的衣服穿了以后,再穿外面的外套 public class TopologicalKahn { Digraph dg; List<Integer> order; //顶点的拓扑顺序 int[] inDegree; public TopologicalKahn(Digraph dg) { this.dg = dg; //Kahn 算法,获取拓扑排序 //1.统计所有顶点入度 //2.将入度为0的作为起点,加入到queue //3.从queue中取出顶点,直到队列为空,将该顶点所指向的顶点入度-1,如果入度=0,则加入队列,循环第三步 //存在环时:输出的顶点数量少于有向图中的顶点数量,或到最后结束循环时,还存在有入度不为0的顶点 order = new LinkedList<>(); //1. inDegree = new int[dg.v()]; for (int u = 0; u < dg.v(); u++) { for (int w : dg.adj(u)) { inDegree[w]++; } } //2.队列中按 成为0入度 的顺序加入顶点 Queue<Integer> queue = new LinkedList<>(); for (int u = 0; u < dg.v(); u++) if (inDegree[u] == 0) queue.add(u); //3. while (!queue.isEmpty()) { int u = queue.remove(); order.add(u); for (int w : dg.adj(u)) { inDegree[w]--; if (inDegree[w] == 0) //若入度不为0,说明还有其他指向该点的边 queue.add(w); } } //4.若最终输出的入度为0的顶点个数小于 原来有向图中顶点个数,说明存在环 if(order.size() < dg.v()) order = null; this.dg = null; } public boolean isDAG() { //是有向无环图吗? return null != order; } public Iterable<Integer> getOrder() { return order; } public static void main(String[] args) { List<String> books = new LinkedList<>(); books.add("a.c"); books.add("b.c"); books.add("c.c"); books.add("d.c"); SymblowDigraph sd = new SymblowDigraph(books); sd.addEdge("a.c", "b.c"); sd.addEdge("b.c", "c.c"); sd.addEdge("d.c", "b.c"); //编译时 a文件被b文件依赖,b文件被c文件依赖,b文件被d文件依赖 //那么哪个文件被先编译? 被依赖的最多的那个文件(a或d)应该被先编译。 如何得到正确的编译顺序? // a.c -> b.c -> c.c // d.c -> Digraph dg = sd.getGraph(); TopologicalKahn top = new TopologicalKahn(dg); if (top.isDAG()) { System.out.println("拓扑序列. 文件优先编译顺序(被依赖深度高的先被编译)"); for (int i : top.getOrder()) { System.out.println(" " + sd.getSymblow(i)); } } } }
输出
拓扑序列. 文件优先编译顺序(被依赖深度高的先被编译)
a.c
d.c
b.c
c.c
2.深度优先+逆后序
将原有向图,用深度优先,求得顶点的逆后序即可
若原图为 a->b->c<-d
先看深度优先的调用堆栈(起点从顶点a遍历到d,若碰到已经访问过的顶点,则跳过不访问,访问后需要将该顶点标记为已访问)
a
b
c
c
b
a
d
d
调用堆栈每深入一次,前面加一个空格
在说后序:指的是,从调用堆栈退出时将顶点放入队列
那上面的例子就是
a
b
c
c - 1
b - 2
a - 3
d
d - 4
得到的序列为 cbad
然后将该顺序求逆向输出,得到 dabc
dabc就是一条符合原有向图的拓扑排序
//拓扑排序 //无环有向图 是 拓扑排序的前提 //拓扑排序后,顶点所依赖的前驱节点必定都先出现再他前面(这种序列叫做 拓扑序列) //只要满足上述条件的排序输出,都是拓扑序列(所以一个图往往会有多个拓扑序)(这种排序过程叫做 拓扑排序) // //拓扑排序再生活中的应用,比如任务的依赖关系,被依赖的基层任务应该先完成,比如穿几件衣服,一定是先把穿在里面的衣服穿了以后,再穿外面的外套 public class Topological { Digraph dg; DirectedCycle dc; DFOrder dfo; //逆有向图 Iterable<Integer> order; //顶点的拓扑顺序 public Topological(Digraph dg) { this.dg = dg; dc = new DirectedCycle(dg); if (!dc.hasCycle()) { //若存在环,则不计算拓扑排序 dfo = new DFOrder(dg); order = dfo.reversePost(); //拓扑排序会用到深度优先 } this.dg = null; } public boolean isDAG() { //是有向无环图吗? return null != order; } public Iterable<Integer> getOrder() { return order; } public static void main(String[] args) { List<String> books = new LinkedList<>(); books.add("小鱼"); books.add("泥巴"); books.add("赵家六"); books.add("虾子"); books.add("大鱼"); books.add("牧羊犬"); books.add("饲料"); books.add("廉价劳动力"); SymblowDigraph sd = new SymblowDigraph(books); sd.addEdge("泥巴", "虾子"); sd.addEdge("虾子", "小鱼"); sd.addEdge("饲料", "小鱼"); sd.addEdge("小鱼", "大鱼"); sd.addEdge("小鱼", "廉价劳动力"); sd.addEdge("小鱼", "牧羊犬"); sd.addEdge("大鱼", "牧羊犬"); sd.addEdge("廉价劳动力", "牧羊犬"); sd.addEdge("大鱼", "赵家六"); sd.addEdge("牧羊犬", "赵家六"); sd.addEdge("廉价劳动力", "赵家六"); //泥巴被虾子吃,虾子被小鱼吃,饲料被小鱼吃,小鱼被大鱼吃,小鱼被牧羊犬吃,小鱼被廉价劳动力吃,大鱼被牧羊犬吃,大鱼被赵家六吃,廉价劳动力被牧羊犬吃,牧羊犬被赵家六吃,廉价劳动力被赵家六吃 //那么整个食物链的低端(被依赖)到顶端的关系是? Digraph dg = sd.getGraph(); Topological top = new Topological(dg); if (top.isDAG()) { System.out.println("拓扑序列. 被依赖的靠近顶行(并行被依赖的话,前后顺序不重要)"); for (int i : top.getOrder()) { System.out.println(" " + sd.getSymblow(i)); } } } }
产生逆后序
//基于深度优先的顶点排序 public class DFOrder { Digraph dg; boolean[] marked; Queue<Integer> pre; //前序 Queue<Integer> post; //后序 Stack<Integer> reversePost; //逆后序 public DFOrder(Digraph dg) { this.dg = dg; marked = new boolean[dg.v()]; pre = new ArrayDeque<>(); post = new ArrayDeque<>(); reversePost = new Stack<>(); for (int u = 0; u < dg.v(); u++) { if (!marked[u]) { dfs(u); } } this.dg = null; } private void dfs(int u) { pre.add(u); marked[u] = true; for (int v : dg.adj(u)) { if (!marked[v]) dfs(v); } post.add(u); reversePost.add(u); } public Iterable<Integer> pre() { return pre; } public Iterable<Integer> post() { return post; } public Iterable<Integer> reversePost() { List<Integer> rpList = new ArrayList<>(); while(reversePost.size() > 0){ rpList.add(reversePost.pop()); } return rpList; } public static void main(String[] args) { } }
输出:
拓扑序列. 被依赖的靠近顶行(并行被依赖的话,前后顺序不重要)
饲料
泥巴
虾子
小鱼
大鱼
廉价劳动力
牧羊犬
赵家六
2.2. 另一种基于深度优先逆后序的写法:
(可能会直白好理解一些,没有封装太多,专注在逻辑上)
//拓扑排序 //无环有向图 是 拓扑排序的前提 //拓扑排序后,顶点所依赖的前驱节点必定都先出现再他前面(这种序列叫做 拓扑序列) //只要满足上述条件的排序输出,都是拓扑序列(所以一个图往往会有多个拓扑序)(这种排序过程叫做 拓扑排序) // //拓扑排序再生活中的应用,比如任务的依赖关系,被依赖的基层任务应该先完成,比如穿几件衣服,一定是先把穿在里面的衣服穿了以后,再穿外面的外套 public class TopologicalDFSAndCyc { Digraph dg; Stack<Integer> reversePostorder; //顶点的逆后序 LinkedList<Integer> order; //顶点的拓扑顺序 boolean[] inStack; boolean[] marked; public TopologicalDFSAndCyc(Digraph dg) { //拓扑排序会用到深度优先的逆后序 this.dg = dg; inStack = new boolean[dg.v()]; marked = new boolean[dg.v()]; reversePostorder = new Stack<>(); //看是否存在环(dfs调用顺序的堆栈上出现了重复,则说明存在环,那么该引用赋为null) for (int v = 0; v < dg.v(); v++) { if (!marked[v]) dfs(v); } if (reversePostorder != null) { order = new LinkedList<>(); while (!reversePostorder.isEmpty()) order.add(reversePostorder.pop()); //将逆后序输出为正向 } this.dg = null; } private void dfs(int v) { inStack[v] = true; marked[v] = true; for (int u : dg.adj(v)) { if (reversePostorder == null) //已经检测到环存在了,跳出循环 break; if (inStack[u]) { //dfs调用轨迹上存在相同节点,说明存在环 reversePostorder = null; } else if (!marked[u]) { dfs(u); //对未访问过的顶点访问 } } if (reversePostorder != null) //还未检测到环,则加入当前顶点到逆后序 reversePostorder.push(v); inStack[v] = false; } public boolean isDAG() { //是有向无环图吗?(没有环存在吗?) return null != order; } public Iterable<Integer> getOrder() { return order; } public static void main(String[] args) { List<String> books = new LinkedList<>(); books.add("a.c"); books.add("b.c"); books.add("c.c"); books.add("d.c"); SymblowDigraph sd = new SymblowDigraph(books); sd.addEdge("a.c", "b.c"); sd.addEdge("b.c", "c.c"); sd.addEdge("d.c", "b.c"); //编译时 a文件被b文件依赖,b文件被c文件依赖,b文件被d文件依赖 //那么哪个文件被先编译? 被依赖的最多的那个文件(a或d)应该被先编译。 如何得到正确的编译顺序? // a.c -> b.c -> c.c // d.c -> //1. Digraph dg = sd.getGraph(); TopologicalDFSAndCyc top = new TopologicalDFSAndCyc(dg); if (top.isDAG()) { System.out.println("拓扑序列. 被依赖深度高的靠近顶行"); for (int i : top.getOrder()) { System.out.println(" " + sd.getSymblow(i)); } } //2.加入一个环,看输出 sd.addEdge("c.c", "a.c"); dg = sd.getGraph(); top = new TopologicalDFSAndCyc(dg); System.out.println("top.isDAG() " + top.isDAG()); if (top.isDAG()) { System.out.println("拓扑序列. 被依赖深度高的靠近顶行"); for (int i : top.getOrder()) { System.out.println(" " + sd.getSymblow(i)); } } } }
输出:
拓扑序列. 被依赖深度高的靠近顶行 d.c a.c b.c c.c top.isDAG() false
关于深度优先得到拓扑排序的方式有2种
1. 将原图求逆图,然后用深度优先,输出后序
2.不将原图求逆图,用深度优先,输出逆后序
举个例子
原图 a->b->c
方法1.
从a遍历到c
a<-b<-c
a
a - 1
b
b - 2
c
c - 3
输出序列为 abc
方法2.
从a遍历到c
a
b
c
c - 3
b - 2
a - 1
输出序列 abc
所以这两种方式是一样的
从方法2的角度来看方法1,只不过是在每层深度遍历的时候,其逆序(堆栈),被先准备好的逆图搞定了后序的输出顺序
从方法1的角度来看方法2,不过是在每次深度遍历的时候,用堆栈实现了方法1用逆图实现的后序的逆