拓扑排序
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列
有向图能被拓扑排序的充要条件就是它必须是一个有向无环图(DAG:Directed Acyclic Graph)。
实现算法
卡恩算法:卡恩于1962年提出的算法。
Kahn算法维基百科上关于Kahn算法的伪码描述:
L ← Empty list that will contain the sorted elements
S ← Set of all nodes with no incoming edges
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)
此算法关键在于需要维护一个入度为0的顶点的集合:
假设L是存放结果的列表,先找到那些入度为零的节点,把这些节点放到L中,因为这些节点没有任何的父节点。
然后把与这些节点相连的边从图中去掉,再寻找图中的入度为零的节点。对于新找到的这些入度为零的节点来说,
他们的父节点已经都在L中了,所以也可以放入L。重复上述操作,直到找不到入度为零的节点。
如果此时L中的元素个数和节点总数相同,说明排序完成;如果L中的元素个数和节点总数不同,说明原图中存在环,无法进行拓扑排序。
- 如图,初始化后,S集合中存在node A 和 node C,因为A和C的入度为0
- 取出S中的node A,将其存入有序的集合L,然后循环遍历由node A引出的边
- 移除A引出的边AB,并可以得到node B ,如果node B的入度在减去AB边之后为0,那么也将node B放到入度为0的集合S中
- 继续从S中取出node C,重复前三步操作~~~
- 当集合S为空之后,检查图中是否还存在任何边,如果存在的话,说明图中至少存在一条环路。不存在的话则返回结果L,此L中的顺序就是对图进行拓扑排序的结果。
Node节点对象:用于记录节点和边数量
NodeGroup对象:用于记录两节点关系
public class Node {
private Object source;
private int indegreeNum = 0;
public Node(Object source) {
this.source = source;
}
public Object getSource() {
return source;
}
public int getIndegreeNum() {
return indegreeNum;
}
public void addIndegreeNum() {
indegreeNum++;
}
public void subIndegreeNum() {
indegreeNum--;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((source == null) ? 0 : source.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Node other = (Node) obj;
if (source == null) {
if (other.source != null)
return false;
} else if (!source.equals(other.source))
return false;
return true;
}
}
public class NodeGroup {
private Node startNode; // A->B : A=startNode B=endNode
private Node endNode;
private String value;
public NodeGroup(Node startNode, Node endNode) {
this.startNode = startNode;
this.endNode = endNode;
}
public NodeGroup(Node startNode, Node endNode, String value) {
this.startNode = startNode;
this.endNode = endNode;
this.value = value;
}
public boolean containNode(Node node) {
return startNode.equals(node) || endNode.equals(node);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof NodeGroup) {
NodeGroup group = (NodeGroup) obj;
return this.getStartNode().equals(group.getStartNode())
&& this.getEndNode().equals(group.getEndNode());
}
return false;
}
@Override
public int hashCode() {
return super.hashCode();
}
public Node getStartNode() {
return startNode;
}
public void setStartNode(Node startNode) {
this.startNode = startNode;
}
public Node getEndNode() {
return endNode;
}
public void setEndNode(Node endNode) {
this.endNode = endNode;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
Graph图对象,主要是通过addNode方法实现将节点添加进图中
public class Graph {
private Set<Node> graphNodes = new HashSet<Node>();
private Map<Node, Set<Node>> nodeEdge = new HashMap<Node, Set<Node>>();
private List<NodeGroup> nodeGroups = new ArrayList<NodeGroup>();
public NodeGroup findNodeGroup(NodeGroup group) {
for (NodeGroup nodeGroup : nodeGroups) {
if (nodeGroup.equals(group)) {
return nodeGroup;
}
}
return null;
}
public boolean addNode(NodeGroup nodeGroup) {
Node startNode = addToGraphNodes(nodeGroup.getStartNode());
Node endNode = addToGraphNodes(nodeGroup.getEndNode());
if (nodeEdge.containsKey(startNode)
&& nodeEdge.get(startNode).contains(endNode)) {
return false;
} else {
addNodeEdge(startNode, endNode);
addNodeGroup(nodeGroup);
return true;
}
}
private void addNodeGroup(NodeGroup nodeGroup) {
NodeGroup findWeight = findNodeGroup(nodeGroup);
if (findWeight == null) {
nodeGroups.add(nodeGroup);
}
}
private void addNodeEdge(Node startNode, Node endNode) {
if (nodeEdge.containsKey(startNode)) {
nodeEdge.get(startNode).add(endNode);
} else {
Set<Node> temp = new HashSet<Node>();
temp.add(endNode);
nodeEdge.put(startNode, temp);
}
endNode.addIndegreeNum();
}
private Node addToGraphNodes(Node node) {
Node graphNode = findGraphNode(node);
if (graphNode != null) {
return graphNode;
} else {
graphNodes.add(node);
return node;
}
}
private Node findGraphNode(Node node) {
for (Node graphNode : graphNodes) {
if (graphNode.equals(node)) {
return graphNode;
}
}
return null;
}
public Set<Node> getGraphNodes() {
return graphNodes;
}
public Map<Node, Set<Node>> getNodeEdge() {
return nodeEdge;
}
public boolean contain(Node node) {
return graphNodes.contains(node);
}
public boolean isEmptyNode() {
return graphNodes.isEmpty();
}
}
凯恩算法实现,通过Graph初始化对象,然后通过topoSort实现排序,getResult得到排序后的结果
public class KahnTopo {
private List<Node> sortResult; // 用来存储结果集
private Queue<Node> zeroIndegreeNodes; // 用来存储入度为0的顶点
private Graph graph;
// 构造函数,初始化
public KahnTopo(Graph di) {
this.graph = di;
this.sortResult = new ArrayList<Node>();
this.zeroIndegreeNodes = new LinkedList<Node>();
// 对入度为0的集合进行初始化
for (Node node : graph.getGraphNodes()) {
if (node.getIndegreeNum() == 0) {
this.zeroIndegreeNodes.add(node);
}
}
}
// 拓扑排序处理过程
public void topoSort() {
while (!zeroIndegreeNodes.isEmpty()) {
Node zeroIndegreeNode = zeroIndegreeNodes.poll();
// 将当前顶点添加到结果集中
sortResult.add(zeroIndegreeNode);
if (graph.getNodeEdge().keySet().isEmpty()) {
sortResult.addAll(zeroIndegreeNodes);
return;
}
// 遍历由node引出的所有边
Set<Node> nodes = graph.getNodeEdge().get(zeroIndegreeNode);
if (nodes != null) {
for (Node node : nodes) {
node.subIndegreeNum();// 将该边从图中移除,通过减少边的数量来表示
if (0 == node.getIndegreeNum()) {
zeroIndegreeNodes.add(node);// 如果入度为0,那么加入入度为0的集合
}
}
}
graph.getGraphNodes().remove(zeroIndegreeNode);
graph.getNodeEdge().remove(zeroIndegreeNode);
}
// 如果此时图中还存在边,那么说明图中含有环路
if (!graph.getGraphNodes().isEmpty()) {
throw new IllegalArgumentException("Has Cycle !");
}
}
public List<Node> getResult() {
return sortResult;
}
}
基于DFS的拓扑排序
借助深度优先遍历来实现拓扑排序, 维基百科上的伪码:
L ← Empty list that will contain the sorted nodes
S ← Set of all nodes with no outgoing edges
for each node n in S do
visit(n)
function visit(node n)
if n has not been visited yet then
mark n as visited
for each node m with an edgefrom m to ndo
visit(m)
add n to L
DFS的实现使用递归实现。需要注意的是:add n to L,将顶点添加到结果List中的时机是在visit方法即将退出之时。
添加顶点到集合中的时机是在visit方法即将退出之时,而visit方法本身是个递归方法,只要当前顶点还存在边指向其它任何顶点,它就会递归调用visit方法,而不会退出。因此,退出visit方法,意味着当前顶点没有指向其它顶点的边了,即当前顶点是一条路径上的最后一个顶点。