有向图 拓扑排序 文件依赖下的编译顺序该如何确定?

有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用逆图实现的后序的逆

原文地址:https://www.cnblogs.com/cyy12/p/12031469.html