定义
In computer science, a topological sort or topological ordering of a directed graph is a linear ordering of its vertices such that for every directed edge uv from vertex u to vertex v, u comes before v in the ordering. For instance, the vertices of the graph may represent tasks to be performed, and the edges may represent constraints that one task must be performed before another; in this application, a topological ordering is just a valid sequence for the tasks. A topological ordering is possible if and only if the graph has no directed cycles, that is, if it is a directed acyclic graph (DAG). Any DAG has at least one topological ordering, and algorithms are known for constructing a topological ordering of any DAG in linear time.
取自维基百科的定义。上面的定义中有几个关键点:
- 每两个顶点u、v 之间都有一条直接的边进行连接。
- 必须是有向无环图。
- 有向无环图至少有一个拓扑排序的方案,并且能够在线性时间内求解出答案。
英文的 wiki 很多时候比中文的详细太多了。本来想好好梳理一下拓扑排序,现在发现我只要好好意译一下英文的 wiki 就好了。
拓扑排序的代表性应用在于对有时序依赖的任务来进行调度,例如有任务[A,B,C,D,E]待执行,其中任务 A 依赖于任务 C,任务 B 依赖于任务 D,E 无依赖,那么CADBE,CDEBA 都是其合理的拓扑排序答案。
在计算机科学中,拓扑排序在指令调度,表格中的公式求值,逻辑组合,makefile 文件的编译顺序决定、数据序列化、连接器中解决符号依赖等方向的应用越来越重要。甚至被用于以何种顺序去加载拥有外键的数据库的表。
算法
Kahn's algorithm
L ← 包含所有有序元素的空列表
S ← 没有前向边的节点的集合
while S is non-empty do
remove a node n from S
add n to tail of L
for each node m with an edge e from n to m do
remove edge e from the graph
if m has no other incoming edges then
insert m into S
if graph has edges then
return error (graph has at least one cycle)
else
return L (a topologically sorted order)
关键其实在于一开始就要找出所有的无前向边的节点,以这些节点为源头开始遍历,將其在S中移除,遍历过的节点加入列表 L 中,移除当前遍历节点的所有后向边, 遍历当前节点结束之后,如果其子节点变成了新的无前向边的节点,將其加入到 S 中。最后只要集合 S 为空,那么 L 就是我们需要求解的答案。
深度优先算法
L ← 包含所有有序元素的空列表
while exists nodes without a permanent mark do
select an unmarked node n
visit(n)
function visit(node n)
if n has a permanent mark then
return
if n has a temporary mark then
stop (not a DAG)
mark n with a temporary mark
for each node m with an edge from n to m do
visit(m)
remove temporary mark from n
mark n with a permanent mark
add n to head of L
节点有三种状态,未标记、临时标记,永久标记状态。 从未标记过的节点开始遍历,对其进行临时标记,如果其没有前向边的话,將其标记,如果有前向边的话,將此节点打上临时标记的标,先遍历其前向边的节点,如果无前向边的话,將此节点打上永久标记。如果当你发现你要遍历的节点已经被打上临时的标的时候,代表当前图有环。
用简单的例子实践一下这两种算法。
package algorithm;
import java.util.*;
/**
* 拓扑排序
* 邻接链表表示方案
*/
public class TopologicalSort {
public List<Integer> Kahn(Map<Integer, List<Integer>> map, int[] allNode){
List<Integer> list = new ArrayList<>();
Queue<Integer> queue = new ArrayDeque<>();
// 找出所有的不包含前向边的节点
for (Integer i:allNode) {
int hasFront = 0;
for(List<Integer> temp : map.values()){
if(temp.contains(i)){
hasFront = 1;
break;
}
}
if(hasFront == 0){
queue.add(i);
}
}
while (!queue.isEmpty()){
Integer k = queue.poll();
list.add(k);
List<Integer> tempList = map.get(k);
map.remove(k);
if(tempList != null){
for(Integer l : tempList){
int hasFront = 0;
for(List<Integer> temp : map.values()){
if(temp.contains(l)){
hasFront = 1;
break;
}
}
if(hasFront == 0){
queue.add(l);
}
}
}
}
if (!map.isEmpty()){
//有环
return null;
}
return list;
}
public List<Integer> DFS(Map<Integer, List<Integer>> map, int[] allNode){
List<Integer> list = new ArrayList<>();
//0为未标记,1 为临时标记,2 为已标记
HashMap<Integer, Integer> flagMap = new HashMap<>();
for (int i : allNode){
flagMap.put(i, 0);
}
for (int key : flagMap.keySet()){
if(!visit(key,flagMap,map,list)){
list = null;
break;
}
}
return list;
}
public boolean visit(Integer i, Map<Integer, Integer> flagMap, Map<Integer, List<Integer>> map, List<Integer> list){
if(flagMap.get(i) == 2){
return true;
}else if (flagMap.get(i) == 1){
//图有环
return false;
}
//判断当前节点是否有前置节点,有前置节点先遍历前置节点
for (Integer k : map.keySet()){
List<Integer> tempList = map.get(k);
if (tempList.contains(i)){
flagMap.put(i,1);
visit(k, flagMap, map, list);
}
}
flagMap.put(i,2);
list.add(i);
return true;
}
public static void main(String[] args){
// 邻接链表来表示图
int[] allNode = {5,7,3,11,8,2,9,10};
Map<Integer, List<Integer>> map = new HashMap<>();
List<Integer> list5 = new ArrayList<>();
list5.add(11);
map.put(5,list5);
List<Integer> list11 = new ArrayList<>();
list11.addAll(Arrays.asList(new Integer[]{2,9,10}));
map.put(11,list11);
List<Integer> list7 = new ArrayList<>();
list7.addAll(Arrays.asList(new Integer[]{11,8}));
map.put(7,list7);
List<Integer> list8 = new ArrayList<>();
list8.add(9);
map.put(8,list8);
List<Integer> list3 = new ArrayList<>();
list3.add(8);
list3.add(10);
map.put(3,list3);
System.out.println("DFS answer is " + new TopologicalSort().DFS(map, allNode));
System.out.println("Kahn answer is" + new TopologicalSort().Kahn(map, allNode));
}
}
输出结果:
DFS answer is [5, 7, 11, 2, 3, 8, 9, 10]
Kahn answer is[5, 7, 3, 11, 8, 2, 10, 9]
写得比较粗糙,深感自己的代码规范还有待提高。
这里的例子是无权重的有向无环图,现实的例子更多的是有权重的有向无环图,在之后的文章里会有拓扑排序实现单源最短路径的查找。
参考文献: