拓扑排序

·  阅读 237

拓扑是什么

这个描述想了半天没想好用来什么来描述, 主要引用百度百科的解释

对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。

网络中也有也有拓扑类的结构, 比如交换机和路由器间建立连接, 手机、电脑等设备和路由器间里连接, 这就构成了一个拓扑结构 image.png

拓扑排序能解决什么样的问题

拓扑排序是图这种数据结构的一个经典算法, 可以用来解决图中点执行的一个顺序问题, 这个讲起来有点抽象, 我下面举两个🌰

穿衣

每早起床, 我们需要穿衣服, 穿衣服一共分为几个步骤, 穿内裤、衬衫、外套、裤子、袜子、鞋子.我们一般都会先穿内衣, 再穿衬衫、裤子, 当然这也有例外, 假如你是SuperMan, 就可以把内裤穿在外面.

image.png

正常的穿衣方案, 可能有下面几种

方案一: 内裤 -> 裤子 -> 衬衫 -> 外套 -> 袜子 -> 鞋子 

方案二: 衬衫 -> 外套 -> 内裤 -> 裤子 -> 袜子 -> 鞋子 
复制代码

其实这就有点像类似拓扑排序了, 因为每类操作之间都有依赖关系,

image.png

拓扑排序如何实现

拓扑排序的基础就是图, 图具体实现就是, 存储边和边与边之间的关联关系, 代码如下

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

image.png

找出图中入度为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算法(理解起来相对复杂, 实现是从下往上)

慢慢来, 比较快

分类:
后端
标签: