1 拓扑排序的实用性
生活中,事物之间总存在着“依赖”关系。而怎么计算依赖关系,这就是拓扑排序的研究内容。我先举个例子(图片来源于网络)
这和先有鸡还是先有蛋是同样的性质。我们用图算法来表示就是:
这一类问题是无法拓扑排序的。只要有环的存在,就无法计算依赖关系。我们再来看另一个过程:
这个过程顺序就明确了,先赚钱,再买房、买车、准备彩礼,最后结婚。上述的婚姻流程图由于不存在环,所以可以计算执行顺序,也就是说,可以计算拓扑排序。此外,在计算依赖的应用中,比如maven、工作流、决策引擎等应用中,拓扑排序都十分重要。
2 入度表算法(也叫BFS算法或Kahn算法)
Kahn算法又叫做入度表算法。这个算法是要找出入度为0的点。入度为0,就是没有有任何节点指向它。也就是只做from,不做to的节点。用大白话说,就是第一步要执行的节点。当处理完了入度为0的点,之后在将这些点删除,于是就有了新的入度为0的点,然后再执行。直到所有点处理完毕,拓扑排序结束。以上述的婚姻流程图为例子:
Step1, 处理入度为0的点:
Step2, 删除为0的点:
Step3, 再处理入度为0的点:
Step4, 再删除入度为0的点:
后续的步骤我就不画图了。所以拓扑排序是:赚钱、买房、买车、彩礼、结婚。可以看出来这是一种BFS搜索算法,所以也叫BFS拓扑排序
3 Java实现
可以使用一个hashmap来表示入度表。因为hashmap key可以为null。
所以可以这样写入度表:
public class DagJoinTable extends HashMap<Integer, List<Integer>> {
public void add(Integer key, Integer value) {
List<Integer> integers = this.computeIfAbsent(key, k -> new ArrayList<>());
// 这里需要避免重复添加
if (!integers.contains(value)) {
integers.add(value);
}
}
public void remove(Integer key, Integer value) {
final List<Integer> integers = get(key);
if (integers != null && !integers.isEmpty()) {
while (integers.remove(value)) {
// 循环删除
}
}
}
}
迭代器的代码:
public class DagIterator<E> implements Iterator<E> {
/**
* key 为 from value 为 to
*/
private DagJoinTable joinTable;
private DirectedAcyclicGraph<E> graph;
/**
* 假设无环图
* 逆连接表为
* A -> B
* B -> C
* C -> D
* 虚构 NULL -> A
*
* @param graph
*/
public DagIterator(DirectedAcyclicGraph<E> graph) {
this.joinTable = new DagJoinTable();
// build逆连接表, 逆连接表里null key的创建特别难啊
// A -> B 这个边,A 放入null KEY B 移出null key
final List<? extends Edge>[] edges = graph.edges;
for (List<? extends Edge> edgePerVertex : edges) {
// 每个点的边
for (Edge edge : edgePerVertex) {
final int from = edge.getFrom();
joinTable.add(null, from);
}
}
for (List<? extends Edge> edgePerVertex : edges) {
for (Edge edge : edgePerVertex) {
final int from = edge.getFrom();
final int to = edge.getTo();
joinTable.add(from, to);
joinTable.remove(null, to);
}
}
this.graph = graph;
}
@Override
public boolean hasNext() {
final List<Integer> integers = joinTable.get(null);
return integers != null && !integers.isEmpty();
}
/**
* NULL -> A
* A -> B,C
* B -> C
* C -> D
* 如果移出A 后
* A 所指向的B 放入NULL KEY
* 变成
* NULL -> B, C
* B -> C
* C -> D
* 这里对于C,因为C存在B->C,所以不能有null -> C
* 所以算法是
*
* @return
*/
@Override
public E next() {
final List<Integer> integers = joinTable.get(null);
if (integers == null || integers.isEmpty()) {
throw new NoSuchElementException();
}
final Integer remove = integers.remove(0);
// 移除之后
final List<Integer> candidates = joinTable.get(remove);
if (candidates != null) {
for (Integer candidate : candidates) {
joinTable.add(null, candidate);
}
for (Integer candidate : candidates) {
final List<Integer> candidateValues = joinTable.get(candidate);
if (candidateValues != null) {
for (Integer candidateValue : candidateValues) {
joinTable.remove(null, candidateValue);
}
}
}
}
if (integers.isEmpty()) {
joinTable.remove(null);
}
return graph.vertices[remove];
}
}
4 Python实现
Python的实现,我没有使用hashmap,而是直接用一个布尔数组连存储入度为0的元素,然后使用双色BFS进行拓扑排序。为了简单,我图的实现是用的邻接矩阵。
# _*_ coding:utf-8 _*_
class UnweightedGraph:
def __init__(self):
self.__vertices = []
self.__edges = []
@property
def vertices(self):
return self.__vertices
@vertices.setter
def vertices(self, value):
self.__vertices = value
@property
def edges(self):
return self.__edges
@edges.setter
def edges(self, value):
self.__edges = value
white = 0
gray = 1
black = 2
def topological_sort(self):
result = []
# 0入度元素
s = [True for _ in self.__vertices]
for edges in self.__edges:
for e in edges:
if e and i in vertices:
circle_exists = False
# 简单BFS
visited = [False for _ in self.__vertices]
queue = [i for i, e in enumerate(s) if e]
while len(queue) > 0:
e = queue.pop()
# 元素
if not visited[e]:
for edge in self.__edges[e]:
if not visited[edge]:
queue.append(edge)
result.append(self.__vertices[e])
visited[e] = True
return result
测试数据:
# _*_ coding:utf-8 _*_
class UnweightedGraph:
def __init__(self):
self.__vertices = []
self.__edges = []
@property
def vertices(self):
return self.__vertices
@vertices.setter
def vertices(self, value):
self.__vertices = value
@property
def edges(self):
return self.__edges
@edges.setter
def edges(self, value):
self.__edges = value
white = 0
gray = 1
black = 2
def topological_sort(self):
result = []
# copy一份点集合,存索引
vertices = [i for i, _ in enumerate(self.__vertices)]
# 当集合内还有元素
while len(vertices) > 0:
# 0入度元素
zero_in_degrees = [True for _ in self.__vertices]
for vertex in vertices:
for e in self.__edges[vertex]:
if zero_in_degrees[e]:
zero_in_degrees[e] = False
# 如果没有0入度表元素则表示存在环
circle_exists = True
for e in zero_in_degrees:
if e:
circle_exists = False
if circle_exists:
raise RuntimeError('存在环')
# 按顺序加入result
for i, e in enumerate(zero_in_degrees):
if e and i in vertices:
# 删除元素
vertices.remove(i)
result.append(self.__vertices[i])
return result
测试结果:
['赚钱', '彩礼', '结婚', '买车', '买房']