题目地址:
https://www.lintcode.com/problem/topological-sorting/description
对一个有向图进行拓扑排序,返回任意一个符合条件的排序。
法1:BFS,即Kahn’s Algorithm。具体做法是,先求出所有顶点的入度,然后取所有入度等于 0 0 0的顶点加入拓扑排序的结果里,接着进行BFS,将这些入度为 0 0 0的顶点的前驱邻边依次删掉,一旦某个顶点的入度删为了 0 0 0,就将其加入拓扑排序的结果里,并将其入队。如此重复操作下去即可。代码如下:
import java.util.*;
public class Solution {
/*
* @param graph: A list of Directed graph node
* @return: Any topological order for the given graph.
*/
public ArrayList<DirectedGraphNode> topSort(ArrayList<DirectedGraphNode> graph) {
// write your code here
// 存储拓扑序答案
ArrayList<DirectedGraphNode> res = new ArrayList<>();
// 存储所有顶点的入度
Map<DirectedGraphNode, Integer> indegrees = getIndegree(graph);
// 为了BFS开一个队列
Queue<DirectedGraphNode> queue = new LinkedList<>();
// 先将所有入度为0的点入队,显然拓扑排序就以这些顶点开始
for (DirectedGraphNode node : graph) {
if (indegrees.get(node) == 0) {
queue.offer(node);
res.add(node);
}
}
while (!queue.isEmpty()) {
// 从队列中取出一个顶点,并删去所有从这个顶点到其邻居节点的邻边
// 一旦发现某个顶点的前驱邻边全删完了,也就是入度变为0了,就加入拓扑序的结果,并将其入队
DirectedGraphNode node = queue.poll();
for (DirectedGraphNode neighbor : node.neighbors) {
indegrees.put(neighbor, indegrees.get(neighbor) - 1);
if (indegrees.get(neighbor) == 0) {
queue.offer(neighbor);
res.add(neighbor);
}
}
}
// 如果拓扑序的结果的大小等于图的顶点个数,说明所有的顶点都已经排序好了
// 说明此图是可以拓扑排序的,返回res;否则返回null
return res.size() == graph.size() ? res : null;
}
// 统计所有顶点的入度
private Map<DirectedGraphNode, Integer> getIndegree(ArrayList<DirectedGraphNode> graph) {
// 初始化一个map,存储所有顶点的入度
Map<DirectedGraphNode, Integer> indegrees = new HashMap<>();
// 先将所有顶点的入度初始化为0
for (DirectedGraphNode node : graph) {
indegrees.put(node, 0);
}
// 接着遍历所有图中的点,将其邻居的入度 + 1
for (DirectedGraphNode node : graph) {
for (DirectedGraphNode neighbor : node.neighbors) {
indegrees.put(neighbor, indegrees.get(neighbor) + 1);
}
}
return indegrees;
}
}
class DirectedGraphNode {
int label;
ArrayList<DirectedGraphNode> neighbors;
DirectedGraphNode(int x) {
label = x;
neighbors = new ArrayList<>();
}
}
时间复杂度 O ( V + E ) O(V+E) O(V+E),空间 O ( V ) O(V) O(V)。
算法正确性证明:
统计所有顶点的入度的方法显然是正确的。先将整个图分成若干个连通块,然后对每个连通块进行考虑。以上算法的做法是,先将入度为
0
0
0的点加入队列和拓扑排序的结果中,接着开始遍历这些顶点,然后对于其后继节点,不停删除其前驱邻边。如果某个顶点的前驱邻边被删干净了,那么上述算法会将其加入最终结果中。这一步显然是正确的,因为其前驱节点一定已经在结果集中了,不违反拓扑排序的定义。接着将其入队。由数学归纳法可以得到,每次加入结果集中的顶点,其前驱节点必然已经存在在结果集里了(当然除了入度为
0
0
0的点,但这不妨碍正确性),并且,所有与入度为
0
0
0的点连通的点,也都加入结果集了。最后如果结果集的大小等于图的顶点个数的话,显然拓扑排序完成。否则的话,说明存在一些点,从入度为
0
0
0的点们出发到这些点的邻边全删掉之后,这些点的入度仍然不为
0
0
0,那么就一定存在环,说明此图不可拓扑排序。证明完毕。
法2:DFS。DFS生成树在递归返回的时候天然生成一个拓扑序的逆序。所以可以用后序遍历的方式,在DFS到某个顶点的时候,将其邻接点全遍历完后,再将其加入一个列表中。代码如下:
import java.util.*;
public class Solution {
/*
* @param graph: A list of Directed graph node
* @return: Any topological order for the given graph.
*/
public ArrayList<DirectedGraphNode> topSort(ArrayList<DirectedGraphNode> graph) {
// write your code here
ArrayList<DirectedGraphNode> res = new ArrayList<>();
// 记录已经访问过的顶点的label
Set<Integer> visited = new HashSet<>();
// 开始对图中每个未访问的顶点进行DFS
for (DirectedGraphNode node : graph) {
if (!visited.contains(node.label)) {
dfs(node, res, visited);
}
}
// 此时res是拓扑序的逆序,需要反一下
Collections.reverse(res);
return res;
}
private void dfs(DirectedGraphNode cur, List<DirectedGraphNode> res, Set<Integer> visited) {
// 标记当前顶点为已经访问过
visited.add(cur.label);
for (DirectedGraphNode neighbor : cur.neighbors) {
if (!visited.contains(neighbor.label)) {
dfs(neighbor, res, visited);
}
}
// 递归返回的时候,意味着cur的子孙节点们都被访问过了,此时将cur加入res,形成一个拓扑序的逆序
res.add(cur);
}
}
时空复杂度 O ( V ) O(V) O(V)。
证明可以考虑任意的两个顶点,由于从 u u u递归返回到 v v v意味着在拓扑序中 v v v在 u u u之前,而在算法中DFS结束后,在res中 u u u恰好在 v v v之前,所以最后反一下的时候就是个拓扑序。所以算法正确。
注解:
DFS求拓扑序不需要求入度,代码很短,比较方便。