拓扑排序:如何确定代码源文件的编译依赖关系?
一个完整的项目往往有很多代码源文件,编译器在编译整个项目的时候,需要按照依赖关系,依次编译每个源文件。编辑器该如何通过源文件两两之间的局部依赖关系确定一个全局的编译顺序呢?
算法解析
什么是拓扑排序?比如在穿衣服的时候都有一定顺序,可以将这个顺序当成衣服衣服之间有一定的依赖关系,在很多时候,拓扑排序序列并不是唯一的。
可以将源文件和源文件之间的依赖关系抽象成一个有向图,每个源文件对应图中的一个顶点,源文件之间的依赖关系就是顶点之间的边。如果a先于b执行,就是说b依赖于a,那么就在顶点a和顶点b之间构建一条从a指向b的边,而且不仅是有向图,还是一个有向无环图,所以拓扑排序就是基于有向无环图的一个算法
public class Graph{
private int v; //顶点个数
private LinkedList<Integer> adj[]; //邻接表
public Graph(int v){
this.v = v;
adj = new LinkedList[v];
for(int i = 0 ; i < v ; ++i){
adj[i] = new LinkedList<>();
}
}
public void addEdge(int s , int t){ //s先于t,边s->t
adj[s].add(t);
}
}
数据结构定义好了,如何在这个有向无环图上,实现拓扑排序?
拓扑排序有两种实现方法,Kahn算法和DFS深度优先搜索算法
1. Kahn算法
用贪心算法思想,定义数据结构的时候,如果某个顶点入度为0,也就表示没有任何顶点必须先于这个顶点执行,那么这个顶点就可以执行。先从图中找出一个入度为0的顶点,将其输出到拓扑排序的结果序列中,并把这个顶点从图中删除(即把这个顶点可达的顶点的入度-1)循环执行上述过程
public void topoSortByKahn(){
int[] inDegree = new int[v]; //统计每个顶点的入度
for(int i = 0 ; i < v ; ++i){
for(int j = 0 ; j < adj[i].size(); ++j){
int w = adj[i].get(j) ; //i ->w
inDegree[w]++;
}
}
LinkedList<Integer> queue = new LinekedList<>();
for(int i = 0 ; i < v ; ++i){
if (inDegree[i] == 0 ) queue.add(i);
}
while(!queue.isEmpty()){
int i = queue.remove();
System.out.print("->" + i);
for(int j = 0 ; j < adj[i].size() ; ++j){
int k = adj[i].get(j);
inDegree[k]--;
if(inDegree[k] == 0 ) queue.add(k);
}
}
}
2.DFS算法
拓扑排序也可以用深度优先搜索来实现,应该是深度优先遍历,遍历图中的所有顶点,而非只是搜索一个顶点到另一个顶点的路径
public void topoSortByDFS(){
//先构建逆邻接表,边s->t表示,s依赖于t,t先于s
LinkedList<Integer> inverseAdj[] = new LinkedList[v];
for(int i = 0 ; i < v ;++i){ //申请空间
inverseAdj[i] = new LinkedList<>();
}
for(int i = 0 ; i < v ; ++i){ //通过邻接表生成逆邻接表
for(int j = 0 ; j < adj[i].size(); ++j){
int w = adj[i].get(j); //i->w
inverseAdj[w].add(i); //w->i
}
}
boolean[] visited = new boolean[v];
for(int i = 0 ; i < v ;++i){ //深度优先遍历图
if(visited[i] == false){
visited[i] = true;
dfs(i , inverseAdj , visited);
}
}
}
private void dfs(int vertex,LinkedList<Integer> inverseAdj[] , boolean[] visited){
for(int i = 0 ; i < inverseAdj[vertex].size();++i){
int w = inverseAdj[vertex].get(i);
if(visited[w] == true) continue;
visited[w] == true;
dfs(w, inverseAdj,visited);
} //先把vertex这个顶点可达的所有顶点都打印出来之后,再打印自己
System.out.print("->" + vertex);
}
算法有两个关键部分:
一是通过邻接表构造逆邻接表
二是递归处理每个顶点,对于顶点vertex来说,先输出它可达的所有顶点,即先把它依赖的所有顶点输出,再输出它自己
总结引申
凡是通过局部顺序来推导全局顺序的,都能用拓扑排序,还可以检测图中是否有环的存在,对于Kahn算法来说,如果最后输出出来的顶点个数小于图中顶点个数,说明存在环
环的检测,因为我们每次都只是查找一个用户的最终推荐人,所以只需要记录已经访问过的用户ID,当用户ID第二次被访问的时候,说明存在环,即存在脏数据
HashSet<Integer> hashTable = new HashSet<>(): //保存已经访问过的用户ID
long findRootReferrerId(long actorId){
if(hashTable.contains(actorId)) { //存在环
return;
}
hashTable.add(actorId);
Long referrerId = select referrer_id from [table] where actor_id = actorId;
if (referrerId == null) return actorId;
return findRootReferrerId(referrerId);
}
在图中,如果a先于b执行,就画一条从a到b的有向边,返回来,如果a先于b执行,画一条从b到a的有向边,表示b依赖a,那么Kahn和DFS算法能否正确工作呢?
可以,构建有向无环图时,要找到出度为0的顶点,然后删除