拓扑排序原理及其实现方法

拓扑排序

在一个大型 Java 工程中,通常存在很多类之间的依赖关系,那在编译整个项目的时候,编译器是如何确定这些类的编译顺序呢?

上面的问题类似于穿衣服的顺序,例如首先要穿内衣,再是裤子,鞋子,毛衣,外套。当然顺序可以有多种并不唯一。

如何确定类的编译顺序,如何得到穿衣顺序,这里面涉及到图论中的拓扑排序算法

拓扑排序原理分析

在穿衣服的例子中,我们可以通过衣服之间的依赖关系得到一个正确的穿衣顺序,在编译很多类的时候,通过类与类之间的依赖关系,可以得到一个正确的类的编译顺序。

很容易发现,拓扑排序的结果并不是唯一的。并且拓扑排序的原理很简单,难的是如何实现拓扑排序。

实现一个算法的第一步是将实际的数据转换成数据结构,这里我们可以将依赖关系转换成中的边,即有向无环图。
有向无环图

有向无环图数据结构:

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) { //t依赖s,存储为s->t,即s先 
		adj[s].add(t);
	}
}

实现拓扑排序的算法有两种:DFS 深度优先搜索Kahn 算法

Kahn 算法

Kahn 算法使用的是贪心算法的思想。

在一个有向无环图中,入度为 0 的排在最前面,其次是入度为 1 的,以此类推。

算法的实现思路是:首先遍历整个图,找到所有入度为 0 的,将它们从图中剔除,同时这些节点指向的其他节点入度都减一。

public void Kahn() {
	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);
			inDegree[w]++;//统计每个顶点的入度 
		}
	}
	LinkedList<Integer> queue = new LinkedList<>();//创建一个队列 
	for (int i=0; i<v; i++) {
		if (inDegree[i] == 0) 
			queue.add(i);//将入度为0的入队 
	}
	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]--;//将这个顶点所指向的所有顶点入度减1 
			if (inDegree[k] == 0) queue.add(k);//若减一后入度为0则入队 
		}
	}
}

DFS 深度优先搜索

这里是利用深度优先搜索的思想实现深度优先遍历,这个算法分为两步操作:

  1. 将邻接表转换成逆邻接表,邻接表中 s->t 表示t依赖 s,在逆邻接表中则转换成了 s->t。
  2. 深度优先遍历逆邻接表,即首先将最深处(一条依赖关系的终点)输出,再递归往前输出其余顶点
public void topoSortByDFS() {
	// 先构建逆邻接表
	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);
			inverseAdj[w].add(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);
	}
	System.out.print("->" + vertex);
}

算法复杂度分析

Kahn 算法中,每个顶点,每条边都被访问了一遍,因此时间复杂度是O(V+E),V是顶点数,E是边数。

DFS 算法中,每个顶点被访问了 2 次,边被访问了一次,因此时间复杂度为O(V+E)

注意:这里的有向无环图中,可以存在多个连通分量,因此边数并不一定大于顶点数。

拓扑排序实际上是用于找到序列的优先顺序,除了使用上述两种算法实现,也可以使用 BFS 广度优先搜索实现。

拓扑排序的应用

  1. 用于确定类的编译顺序
  2. 确定图中是否存在环,若存在环,则输出的顶点数一点小于顶点总数
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值