拓扑排序(Topological Sorting)
Ⅰ 前言
我们一般写程序的时候,一个完整的项目往往会包含很多代码源文件,编译器在编译整个项目的时候,需要按照依赖关系,依次编译每个源文件。比如 a.java 依赖 b.java,那在编译的时候,编译器需要先编译 b.java,才能编译 a.java。
原来我写 C 语言的程序的时候,经常要用到联合编译,当时我们运行程序都是手动在命令行上编译和运行,所以如果要联合编译而且有依赖关系,比如 a.c 里用到了 b.c 的函数,我们就要先把 b.c 编译,生成一个 obj 文件,然后再用 b.obj 和 a.c 一起编译。大家如果看我最早的文章,一定会经常看到。现在用 Java 就舒服很多,IDE 会自动编译这个文件所依赖的其他文件,不用我再一个一个按照依赖关系手动编译了。
编译器通过分析源文件或者程序员事先写好的编译配置文件(比如 Makefile)文件,来获取这种局部的依赖关系。那么编译器又该如何通过源文件两两之间的局部依赖关系,确定一个全局的编译顺序呢?
这就要用到我们这篇文章要讲的这个算法了。
Ⅱ 拓扑排序算法解析
上面提出的问题的解决思路与 “图” 这种数据结构的一个经典算法 “拓朴排序算法” 有关。那什么是拓扑排序呢?这个概念很好理解,我们来看一个生活中的拓扑排序的例子。
我们在穿衣服的时候都有一定的顺序,我们可以把这种顺序想成,衣服与衣服之间有一定的依赖关系。比如说,你必须先穿内裤,才能穿外裤,不能反过来,不是每个人都能穿出超人的感觉。
假设我们现在有八件衣服要穿,它们之间的两两依赖关系我们已经很清楚了,那如何安排一个穿衣序列,能够满足所有的两两之间的依赖关系?
这就是个拓朴排序的问题。从这个例子里,你应该能想到,在很多时候,拓扑排序的序列并不是唯一的。你可以看一下下面这张图,这两种排序都满足这些局部先后关系的穿衣序列。
弄懂了生活中的这个例子,你应该对开篇讲的编译依赖关系有思路了,它和这个问题一样,都可以抽象成一个拓扑排序问题。
拓扑排序的原理非常简单,我们的重点放在拓扑排序的实现上面。
我们知道,算法是构建在具体的数据结构上的,针对这个问题,我们先来看看,如何将问题背景抽象成具体的数据结构。
如果 a 先于 b 执行,也就是说 b 依赖于 a,那么就在顶点 a 和顶点 b 之间,构建一条从 a 指向 b 的边。而且,这个图不仅要是有向图,还要是一个有向无环图,也就是不能存在像 a->b->c->d->a 这样的循环依赖关系。因为图中一旦出现环,拓扑排序就无法工作了。实际上,拓扑排序本身就是基于有向无环图的一个算法。
package com.tyz.about_topo.core;
import java.util.LinkedList;
/**
* 构造有向无环图
* @author Tong
*/
public class Graph {
private int vertex; //顶点数
private LinkedList<Integer> adj[]; //邻接表
public Graph() {
}
@SuppressWarnings("unchecked")
public Graph(int vertex) {
this.vertex = vertex;
this.adj = new LinkedList[this.vertex];
for (int i = 0; i < vertex; i++) {
this.adj[i] = new LinkedList<Integer>();
}
}
public void addEdge(int start, int end) {
//加有向边
this.adj[start].add(end);
}