拓扑是什么
这个描述想了半天没想好用来什么来描述, 主要引用百度百科的解释
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
网络中也有也有拓扑类的结构, 比如交换机和路由器间建立连接, 手机、电脑等设备和路由器间里连接, 这就构成了一个拓扑结构
拓扑排序能解决什么样的问题
拓扑排序是图这种数据结构的一个经典算法, 可以用来解决图中点执行的一个顺序问题, 这个讲起来有点抽象, 我下面举两个🌰
穿衣
每早起床, 我们需要穿衣服, 穿衣服一共分为几个步骤, 穿内裤、衬衫、外套、裤子、袜子、鞋子.我们一般都会先穿内衣, 再穿衬衫、裤子, 当然这也有例外, 假如你是SuperMan, 就可以把内裤穿在外面.
正常的穿衣方案, 可能有下面几种
方案一: 内裤 -> 裤子 -> 衬衫 -> 外套 -> 袜子 -> 鞋子
方案二: 衬衫 -> 外套 -> 内裤 -> 裤子 -> 袜子 -> 鞋子
其实这就有点像类似拓扑排序了, 因为每类操作之间都有依赖关系,
拓扑排序如何实现
拓扑排序的基础就是图, 图具体实现就是, 存储边和边与边之间的关联关系, 代码如下
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
public class Graph {
private int v; // 顶点的个数
private LinkedList<Integer> adj[]; // 邻接表
public Graph(int v) {
this.v = v;
adj = new LinkedList[v];
for (int i = 0; i < v; i++) {
adj[i] = new LinkedList<>();
}
}
/**
* 增加有向边
* @param dot 点
* @param pointedDot 被指向的点,即被依赖的点
*/
public void insertEdge(int dot, int pointedDot) {
adj[pointedDot].add(dot);
}
}
拓扑排序具体的解决方案有两种, 一种是Kahn 算法, 另一种是DFS深度优先遍历
Kanh算法
Kanh核心思想就是贪心 根据图中的边建立每个顶点的入度, 即这个顶点依赖的顶点的个数, 如顶点0没有依赖的订单, 故度为0; 顶点1需依赖顶点0和顶点2, 故度为2
找出图中入度为0的节点, 输出并删除(就是将依赖该顶点的节点的入度-1), 再循环执行上述过程, 直到所有顶点都被输出, 这种可以借助队列来执行, 具体代码如下
public void kahnTopoLogical() {
/**
* 统计每个顶点的入度
*/
int[] inDegree = new int[v];
for (int i = 0; i < v; ++i) {
for (int j : adj[i]) {
inDegree[j]++;
}
}
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < v; ++i) {
// 若顶点的入度为0,则放入队列中
if (inDegree[i] == 0) {
queue.offer(i);
}
}
while (!queue.isEmpty()) {
int dot = queue.poll();
System.out.println("->" + dot); // 打印顶点
for (int nextDot : adj[dot]) {
--inDegree[nextDot];
if (inDegree[nextDot] == 0) {
queue.offer(nextDot);
}
}
}
}
DFS算法
如果说kahn算法是自顶而下的, 那么DFS算法就是自下而上的, 遍历到的节点, 如果该节点还依赖于其他顶点, 则先遍历依赖的节点, 这个有点类似于图的DFS深度优先遍历了, 其中的思想也都是类似, 代码如下
public void topoSortByDFS() {
/**
* 有向边存储的是 A <- B的关系
* 因为我们如果遍历到节点, 还需要遍历他依赖的节点
* 所以我们要存储逆向的关系, 即 B -> A的关系
*/
List<Integer>[] inverseAdj = new ArrayList[v];
for (int i = 0; i < v; ++i) {
inverseAdj[i] = new ArrayList<>();
}
for (int i = 0; i < v; i++) {
for (int j : adj[i]) {
inverseAdj[j].add(i);
}
}
boolean[] visited = new boolean[v]; // 用来存储当前节点是否被访问过
for (int i = 0; i < v; ++i) {
if (visited[i] == false) {
visited[i] = true;
dfs(i, inverseAdj, visited);
}
}
}
private void dfs(int vertex, List<Integer>[] inverseAdj, boolean[] visited) {
for (int dot : inverseAdj[vertex]) {
if (visited[dot] == true) continue;
dfs(dot, inverseAdj, visited);
visited[dot] = true;
}
System.out.println("->" + vertex);
}
注意: 这里面打印出来的点之间是不连续的, 因为操作顶点点, 会直接往上遍历下面的节点, 而遍历底下的顶点
拓扑排序解决工作当中的需求🌰
服务组合上线
本人在工作当中也遇到过一个拓扑排序的场景. 需求方提了一个多个服务组合上线的需求
场景是原有平台只支持单个服务上线, 但实际上线过程中, 一个产品的上线会涉及到多个服务的更改以及上线, 用户可能会漏掉本应一起上线的服务, 导致上线失败, 所以平台要支持多个服务组合上线的功能
用户在页面上可以选择多个服务, 并可以设置服务间的关联关系, 点击上线时将这些服务一起上线
这就要求后端根据前端设置的服务间的依赖关系, 判断服务间是否存在循环依赖的情况, 在点击发布的时候, 后台会根据服务的依赖关系进行顺序上线, 如服务A若依赖了服务B, 那么必须先上线服务B, 等服务B上线成功之后, 再上线服务A. 正好拓扑排序刚好可以解决这两个问题
- 服务间是否存在循环依赖, 即有向图中是否存在环
- 服务间上线顺序需要根据对应的依赖关系上线, 对应的就是拓扑排序, 遍历
具体实现
服务间是否存在循环依赖
使用kahn算法实现, 使用了一些java8的stream, 代码如下
@Data
public class ServiceDependencyDto {
private Integer serviceId;
private String serviceName;
private Integer releaseVersionId;
private String serviceVersion;
private String status;
private List<ServiceDependencyDto> dependencies;
}
/**
* 校验服务是否存在循环依赖
*/
private static void checkContainsCycle(List<ServiceDependencyDto> dtos) throws BaseException {
final Map<String, Integer> inDegreeMap =
dtos.stream().collect(Collectors.toMap(ServiceDependencyDto::getServiceName, e -> 0));
dtos.stream()
.flatMap(dto -> dto.getDependencies().stream())
.forEach(
dto -> {
final String serviceName = dto.getServiceName();
inDegreeMap.compute(serviceName, (k, v) -> v == null ? 1 : v + 1);
});
final Map<String, List<ServiceDependencyDto>> dependencyMap =
dtos.stream()
.collect(
Collectors.toMap(
ServiceDependencyDto::getServiceName, ServiceDependencyDto::getDependencies));
final List<String> queue =
inDegreeMap.entrySet().stream()
.filter(entry -> entry.getValue() == 0)
.map(Entry::getKey)
.collect(Collectors.toList());
int visited = 0;
for (int i = 0; i < queue.size(); i++) {
visited++;
dependencyMap
.get(queue.get(i))
.forEach(
dto -> {
final String serviceName = dto.getServiceName();
int inDegree = inDegreeMap.get(serviceName);
inDegree--;
inDegreeMap.put(serviceName, inDegree);
if (inDegree <= 0) {
queue.add(serviceName);
}
});
}
if (visited != dtos.size()) {
throw new BaseException(CONTAINS_CYCLE);
}
}
服务组合发布的顺序
这个后续再写一遍来讲服务组合发布了
总结
拓扑排序就是图相关的算法, 用来解决每个点(放在实际场景就是具体的操作)之间的执行顺序问题, 还可以用来解决图是否存在环等等, 具体的实现有贪心的Kahn算法, 也有DFS算法(理解起来相对复杂, 实现是从下往上)
慢慢来, 比较快