拓扑排序
问题描述
定义:将有向图中的顶点以线性方式进行排序。即对于任何连接自顶点u到顶点v的有向边uv,在最后的排序结果中,顶点u总是在顶点v的前面。
根据以上定义可以知道,拓扑排序是针对有向无环图(DAG:Directed Acyclic Graph)中的顶点顺序进行排序,举一个例子:选课。我想任何看过数据结构相关书籍的同学都知道它吧。假设我非常想学习一门机器学习的课程,但是在修这么课程之前,我们必须要学习一些基础课程,比如计算机科学概论,C语言程序设计,数据结构,算法等等。那么这个制定选修课程顺序的过程,实际上就是一个拓扑排序的过程,每门课程相当于有向图中的一个顶点,而连接顶点之间的有向边就是课程学习的先后关系。只不过这个过程不是那么复杂,从而很自然的在我们的大脑中完成了。将这个过程以算法的形式描述出来的结果,就是拓扑排序。
那么是不是所有的有向图都能够被拓扑排序呢?显然不是。继续考虑上面的例子,如果告诉你在选修计算机科学概论这门课之前需要你先学习机器学习,你是不是会被弄糊涂?在这种情况下,就无法进行拓扑排序,因为它中间存在互相依赖的关系,从而无法确定谁先谁后。在有向图中,这种情况被描述为存在环路。因此,一个有向图能被拓扑排序的充要条件就是它是一个有向无环图(DAG:Directed Acyclic Graph)。
算法实现
1 kahn算法
这种算法的核心在于维护一个入度为零的顶点队列,步骤如下:
1 先遍历一遍所有的点,把所有入度为0的顶点放入队列中
2 在队列中取出一个顶点放入结果集中。
3 遍历从该点出发的所有边的另外一个点,将这个点的入度-1,然后判断该点是否是入度为0。
4 为0,则将该点放入上述队列中,遍历下一个边;否则,不取该点到队列,遍历下一个边。
5 如果这个点的边遍历完了,且队列不为空,就返回第2步,继续执行。队列空,则结束循环。
代码如下:
package test; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; /** * 拓扑排序,当前方案并没有在节点类中加入过多的内容 * 但是在图类中加入了边的集合adjaNode */ public class TopoSortB { /** * 拓扑排序节点类 */ private static class Node { public Object val; public int pathIn = 0; // 入链路数量 public Node(Object val) { this.val = val; } } /** * 拓扑图类 */ private static class Graph { // 图中节点的集合 public Set<Node> vertexSet = new HashSet<Node>(); // 相邻的节点,纪录边 public Map<Node, Set<Node>> adjaNode = new HashMap<Node, Set<Node>>(); // 将节点加入图中 public boolean addNode(Node start, Node end) { //如果没有起点/终点,把起点/终点加入到点的集合中 if (!vertexSet.contains(start)) { vertexSet.add(start); } if (!vertexSet.contains(end)) { vertexSet.add(end); } //如果在边的集合中存在这条边,返回false if (adjaNode.containsKey(start) && adjaNode.get(start).contains(end)) { return false; } //如果在边的集合中不存在这条边,但是存在以start为起点的其他边,则直接添加以end为终点的边,否则,添加新的边 if (adjaNode.containsKey(start)) { adjaNode.get(start).add(end); } else { Set<Node> temp = new HashSet<Node>(); temp.add(end); adjaNode.put(start, temp); } end.pathIn++; //end的入度+1 return true; } } //Kahn算法 private static class KahnTopo { private List<Node> result; // 用来存储结果集 private Queue<Node> setOfZeroIndegree; // 用来存储入度为0的顶点 private Graph graph; //传入一张图 //构造函数,初始化 public KahnTopo(Graph di) { this.graph = di; this.result = new ArrayList<Node>(); this.setOfZeroIndegree = new LinkedList<Node>(); // 对入度为0的集合进行初始化 for(Node iterator : this.graph.vertexSet){ if(iterator.pathIn == 0){ this.setOfZeroIndegree.add(iterator); } } } //拓扑排序处理过程 private void process() { //遍历入度为0的队列,并且动态添加每次遍历后新产生的入度为0的点到队列中 while (!setOfZeroIndegree.isEmpty()) { Node v = setOfZeroIndegree.poll(); // 将当前顶点添加到结果集中 result.add(v); //adjaNode(Node, set<Node>)是边的集合,下边是判断边集合是否还有key值,即是否为空 if(this.graph.adjaNode.keySet().isEmpty()){ return; } // 遍历由v引出的所有边 ,目的是把v放入结果集同时把以v为起点的所有终点的入度-1 for (Node w : this.graph.adjaNode.get(v) ) { // 将该边从图中移除,通过减少边的数量来表示 w.pathIn--; if (0 == w.pathIn) // 如果入度为0,那么加入入度为0的集合 { setOfZeroIndegree.add(w); } } this.graph.vertexSet.remove(v); //点集移除一个点 this.graph.adjaNode.remove(v); //边集移除以此点为起点的所有边 } // 如果此时图中还存在边,那么说明图中含有环路 if (!this.graph.vertexSet.isEmpty()) { throw new IllegalArgumentException("Has Cycle !"); } } //结果集 public Iterable<Node> getResult() { return result; } } //测试 public static void main(String[] args) { Node A = new Node("A"); Node B = new Node("B"); Node C = new Node("C"); Node D = new Node("D"); Node E = new Node("E"); Node F = new Node("F"); Graph graph = new Graph(); graph.addNode(A, B); graph.addNode(B, C); graph.addNode(B, D); graph.addNode(D, C); graph.addNode(E, C); graph.addNode(C, F); KahnTopo topo = new KahnTopo(graph); topo.process(); for(Node temp : topo.getResult()){ System.out.print(temp.val.toString() + " "); } } }
2 基于DFS的算法
除了使用上面直观的Kahn算法之外,还能够借助深度优先遍历来实现拓扑排序。这个时候需要使用到栈结构来记录拓扑排序的结果。
通过递归方法,遍历整个图,每个点设置一个标志位,标志着这个点是否已经放入结果集中。每当遇到标志显示未放入结果集且没有以该点为起点的边时,把该点放入结果集中,这样,通过递归,放入结果集的前后顺序,就是图中点的遍历顺序。
实例代码:
package test; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; public class TopoSortC { /** * 拓扑排序节点类 */ private static class Node { public Object val; public boolean mark = false; // 该点是否被遍历过 默认是false没被遍历过 public Node(Object val) { this.val = val; } } /** * 拓扑图类 */ private static class Graph { // 图中节点的集合 public Set<Node> vertexSet = new HashSet<Node>(); // 相邻的节点,纪录边 public Map<Node, Set<Node>> adjaNode = new HashMap<Node, Set<Node>>(); // 将节点加入图中 public boolean addNode(Node start, Node end) { //如果没有起点/终点,把起点/终点加入到点的集合中 if (!vertexSet.contains(start)) { vertexSet.add(start); } if (!vertexSet.contains(end)) { vertexSet.add(end); } //如果在边的集合中存在这条边,返回false if (adjaNode.containsKey(start) && adjaNode.get(start).contains(end)) { return false; } //如果在边的集合中不存在这条边,但是存在以start为起点的其他边,则直接添加以end为终点的边,否则,添加新的边 if (adjaNode.containsKey(start)) { adjaNode.get(start).add(end); } else { Set<Node> temp = new HashSet<Node>(); temp.add(end); adjaNode.put(start, temp); } return true; } } public static void visit(Graph graph, List<Node> result, Node node){ if(node.mark) return;//如果被遍历过,直接结束遍历 node.mark = true; if(graph.adjaNode.containsKey(node)) {//如果没有以 for(Node n : graph.adjaNode.get(node)){ visit(graph, result, n); } } result.add(node); } public static List<Node> sortDFS(Graph graph){ List<Node> result = new ArrayList<>(); for(Node node : graph.vertexSet){ visit(graph, result, node); } return result; } //测试 public static void main(String[] args) { Node A = new Node("A"); Node B = new Node("B"); Node C = new Node("C"); Node D = new Node("D"); Node E = new Node("E"); Node F = new Node("F"); Graph graph = new Graph(); graph.addNode(A, B); graph.addNode(B, C); graph.addNode(B, D); graph.addNode(D, C); graph.addNode(E, C); graph.addNode(C, F); List<Node> list = sortDFS(graph); for(Node node : list){ System.out.print(node.val + " "); } } }