1 定义和前置条件
1.1 定义
将有向图中的顶点以线性方式进行排序。即对于任何连接自顶点u到顶点v的有向边uv,在最后的排序结果中,顶点u总是在顶点v的前面。
假设我非常想学习一门机器学习的课程,但是在修这么课程之前,我们必须要学习一些基础课程,比如计算机科学概论,C语言程序设计,数据结构,算法等等。那么这个制定选修课程顺序的过程,实际上就是一个拓扑排序的过程,每门课程相当于有向图中的一个顶点,而连接顶点之间的有向边就是课程学习的先后关系。只不过这个过程不是那么复杂,从而很自然的在我们的大脑中完成了。将这个过程以算法的形式描述出来的结果,就是拓扑排序。
1.2 前置条件
一个有向图能被拓扑排序的充要条件就是它是一个有向无环图(DAG:Directed Acyclic Graph)。(即不存在环路)
2 分析
原图:
拓扑图图解:
拓扑排序结果:
顶点:v3
顶点:v1
顶点:v2
顶点:v6
顶点:v9
顶点:v10
顶点:v13
顶点:v0
顶点:v4
顶点:v5
顶点:v8
顶点:v7
顶点:v12
顶点:v11
3 典型实现算法
3.1 Kahn算法
3.1.1 伪代码
L←将包含已排序元素的空列表
S←没有传入边的所有节点的集合
while(S非空) {
从S中删除节点n
将n插入L
对于具有从n到m的边e的每个节点m
从图中删除边e
如果m在下降沿没有其他边
然后将m插入到S中
}
如果图形有边
然后返回错误(图表至少有周期)
其他
return L(一个拓扑有序序列)
2.1.2 代码
package com.dn.dijstra;
import java.util.ArrayList;
import java.util.Stack;
public class DnGraphTopologic {
private int numVertexes;
private VertexNode [] adjList;//邻接顶点的一维数组
public DnGraphTopologic(int numVertexes){
this.numVertexes = numVertexes;
}
private void createGraph(){
VertexNode node0 = new VertexNode(0,"v0");//VertexNode(int in,String data)
VertexNode node1 = new VertexNode(0,"v1");
VertexNode node2 = new VertexNode(2,"v2");
VertexNode node3 = new VertexNode(0,"v3");
VertexNode node4 = new VertexNode(2,"v4");
VertexNode node5 = new VertexNode(3,"v5");
VertexNode node6 = new VertexNode(1,"v6");
VertexNode node7 = new VertexNode(2,"v7");
VertexNode node8 = new VertexNode(2,"v8");
VertexNode node9 = new VertexNode(1,"v9");
VertexNode node10 = new VertexNode(1,"v10");
VertexNode node11 = new VertexNode(2,"v11");
VertexNode node12 = new VertexNode(1,"v12");
VertexNode node13 = new VertexNode(2,"v13");
adjList = new VertexNode[numVertexes];
adjList[0] =node0;
adjList[1] =node1;
adjList[2] =node2;
adjList[3] =node3;
adjList[4] =node4;
adjList[5] =node5;
adjList[6] =node6;
adjList[7] =node7;
adjList[8] =node8;
adjList[9] =node9;
adjList[10] =node10;
adjList[11] =node11;
adjList[12] =node12;
adjList[13] =node13;
node0.firstEdge = new EdgeNode(11);node0.firstEdge.next = new EdgeNode(5);node0.firstEdge.next.next = new EdgeNode(4);
node1.firstEdge = new EdgeNode(8);node1.firstEdge.next = new EdgeNode(4);node1.firstEdge.next.next = new EdgeNode(2);
node2.firstEdge = new EdgeNode(9);node2.firstEdge.next = new EdgeNode(6);node2.firstEdge.next.next = new EdgeNode(5);
node3.firstEdge = new EdgeNode(13);node3.firstEdge.next = new EdgeNode(2);
node4.firstEdge = new EdgeNode(7);
node5.firstEdge = new EdgeNode(12);node5.firstEdge.next = new EdgeNode(8);
node6.firstEdge = new EdgeNode(5);
node8.firstEdge = new EdgeNode(7);
node9.firstEdge = new EdgeNode(11);node9.firstEdge.next = new EdgeNode(10);
node10.firstEdge = new EdgeNode(13);
node12.firstEdge = new EdgeNode(9);
}
/**
* 拓扑排序
*
* 核心思想:每次从该栈中取出一个顶点,图形添加到拓扑排序的数量+1,将该顶点放入保存结果的集合中。
* 紧接着循环遍历由该顶点引出的所有边,该顶点的入度减1;如果该顶点的入度在减去本条边之后为0,也将这个顶点放到入度为0的栈中。然后继续从栈中取出一个顶点…………
* 当图形添加到拓扑排序的数量<图形的边,说明图中至少存在一条环路,抛出异常。相等的话则返回结果集合,此结果集合中的顺序就是对图进行拓扑排序的结果。
*
*/
private ArrayList<String> topologicalSort() throws Exception{
Stack<Integer> stack = new Stack<>();//存入入度为0边节点的栈
ArrayList<String> array = new ArrayList<String>();//拓扑有序序列
int count = 0;//图形添加到拓扑排序的数量
int k = 0;
for(int i = 0;i<numVertexes;i++ ){
if(adjList[i].in == 0){//入度
stack.push(i);//入度为0的顶点入栈
}
}
while(!stack.isEmpty()){
int pop = stack.pop();//从栈中取出一个顶点
array.add(adjList[pop].data);//添加到拓扑有序序列中
count++;
//循环遍历由该顶点引出的所有边
for(EdgeNode node = adjList[pop].firstEdge;node!=null;node = node.next){
k = node.adjVert;//顶点下标
if(--adjList[k].in == 0){//入度-1是否等于0
stack.push(k);//入度为0,入栈
}
}
}
if(count<numVertexes){//图形添加到拓扑排序的数量<图形的边,即是图形有边
throw new Exception("完犊子了,拓扑排序失败");
}
//否则返回一个拓扑有序序列
return array;
}
/**
* 边表顶点
*/
class EdgeNode{
private int adjVert;//顶点下标
private EdgeNode next;//next指针
private int weight;//权重
public EdgeNode(int adjVert){
this.adjVert = adjVert;
}
public int getAdjVert() {
return adjVert;
}
public void setAdjVert(int adjVert) {
this.adjVert = adjVert;
}
public EdgeNode getNext() {
return next;
}
public void setNext(EdgeNode next) {
this.next = next;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
}
/**
* 邻接顶点
*/
class VertexNode{
private int in;//入度数量
private String data;//顶点名称
private EdgeNode firstEdge;//边表顶点
public VertexNode(int in,String data){
this.in = in;
this.data = data;
}
public int getIn() {
return in;
}
public void setIn(int in) {
this.in = in;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public EdgeNode getFirstEdge() {
return firstEdge;
}
public void setFirstEdge(EdgeNode firstEdge) {
this.firstEdge = firstEdge;
}
}
public static void main(String [] args){
DnGraphTopologic dnGraphTopologic = new DnGraphTopologic(14);
dnGraphTopologic.createGraph();
try {
ArrayList<String> array = dnGraphTopologic.topologicalSort();
//遍历输入排序顶点
for(int i = 0;i<array.size();i++ ){
System.out.println("顶点:"+array.get(i));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.2 基于DFS的拓扑排序
3.2.1 伪代码
L←将包含排序的节点的空列表
S←没有输出边的所有节点的集合
对于S中的每个节点n
访问(n)
函数访问(节点n)
如果n还没有被访问过
标记为已访问
对于具有从m到ndo的边的每个节点m
访问(m)
将n添加到L.
3.2.2 分析
DFS的实现更加简单直观,使用递归实现。利用DFS实现拓扑排序,实际上只需要添加一行代码,即上面伪码中的最后一行:add n to L。