有向无环图孤岛和环校验以及深度遍历与广度遍历
有向无环图孤岛和环校验以及深度遍历与广度遍历
1 背景
在阿里巴巴做项目时用到的图,这里抽象出来有关算法和数据结构部分,不带有其他业务信息
2 有向无环图的约束
- 只有一个起始节点
- 有多个结束节点,且结束节点的数量为b
- 每个节点不能有到自身的边
- 每个节点到另外一个节点最多一条边
- 每个边仅有一个条件,或者没有条件,没条件用空字符串表示
- 没有重复的边和重复的点
3 有向无环图实体类
1.起始节点的id放在图中
2.结束节点的下一个节点的集合中仅有一个条件为空字符串的边,且指向节点的Id为-1L
3.节点
/**
* 节点
*/
private static class Node {
/**
* id
*/
Long id;
/**
* 下一个节点的集合
*/
Map<String, Long> nextNodeIdMap;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Map<String, Long> getNextNodeIdMap() {
return nextNodeIdMap;
}
public void setNextNodeIdMap(Map<String, Long> nextNodeIdMap) {
this.nextNodeIdMap = nextNodeIdMap;
}
}
4.有向边(这里考虑前端需要,自己组装了边,其实可以从Node.nextNodeIdMap中得出)
/**
* 有向边
*/
private static class DirectionConditionEdge {
/**
* 起点
*/
Long fromNodeId;
/**
* 终点
*/
Long toNodeId;
/**
* 条件
*/
String condition;
public Long getFromNodeId() {
return fromNodeId;
}
public void setFromNodeId(Long fromNodeId) {
this.fromNodeId = fromNodeId;
}
public Long getToNodeId() {
return toNodeId;
}
public void setToNodeId(Long toNodeId) {
this.toNodeId = toNodeId;
}
public String getCondition() {
return condition;
}
public void setCondition(String condition) {
this.condition = condition;
}
}
- 图
public class Graph {
/**
* 起点
*/
Long startNodeId;
/**
* 所有点的集合
*/
Set<Node> nodeSet;
/**
* 所有边的集合
*/
Set<DirectionConditionEdge> directionConditionEdgeSet;
public Long getStartNodeId() {
return startNodeId;
}
public void setStartNodeId(Long startNodeId) {
this.startNodeId = startNodeId;
}
public Set<Node> getNodeSet() {
return nodeSet;
}
public void setNodeSet(Set<Node> nodeSet) {
this.nodeSet = nodeSet;
}
public Set<DirectionConditionEdge> getDirectionConditionEdgeSet() {
return directionConditionEdgeSet;
}
public void setDirectionConditionEdgeSet(Set<DirectionConditionEdge> directionConditionEdgeSet) {
this.directionConditionEdgeSet = directionConditionEdgeSet;
}
}
4 环校验和孤岛节点校验
-
- 环校验核心思想 :遍历每条path(起始节点到结束节点之间的路径称之为path),查看path中有没有重复的点。如果有重复的点,则有环,没有重复的节点,则没有环。
-
- 孤岛节点校验核心思想:从起始节点出发,根据边,找到所有有可能到达的节点。对比,所有的节点,如果有节点不可到达,则有孤立节点,如果没有节点不可达,则没有孤岛节点。
-
- 校验的具体实现:使用递归和试探回溯算法进行验证。
- 3.1 从步骤1和步骤2我们不难看出,遍历的过程需要一个存储路径上的节点的集合用于环校验(pathSet),还需要一个存储已经遍历过的节点的集合用于孤岛节点校验(allReachableSet)
- 3.2 使用递归的理由对于每个节点,处理的情况基本一致。
- 3.3 每到一个节点时
- 3.3.1 如果该节点在pathSet中,则报错,该路径有环。如果该节点不在pathSet中,则将该节点加入路径中。
- 3.3.2 allReachableSet加入该节点
- 3.3.3 如果该节点没有下一个节点,则回溯:从pathSet中移除当前节点,回到上一个处理的节点
- 3.3.4 对该节点的所有下一个节点,递归调用 3.3
- 3.3.5 对该节点的所有下一个节点,处理完后,从pathSet中移除当前节点,回到上一个处理的节点
-
- 代码实现:
private void validateCycleAndFindAllNodeCanBeReachFromThisNode(Long currentNodeId, Map<Long, Set<Long>> directionEdgeMap, Set<Long> reachableNodeIdSetFromStartNode, Set<Long> reachableNodeIdByPathSetFromStartNode) {
if (reachableNodeIdByPathSetFromStartNode.contains(currentNodeId)) {
throw new IllegalArgumentException("有环");
}
reachableNodeIdSetFromStartNode.add(currentNodeId);
reachableNodeIdByPathSetFromStartNode.add(currentNodeId);
Set<Long> nextNodeSet = directionEdgeMap.get(currentNodeId);
if (null == nextNodeSet || nextNodeSet.size() == 0) {
reachableNodeIdByPathSetFromStartNode.remove(currentNodeId);
return;
}
for (Long nextNodeId : nextNodeSet) {
validateCycleAndFindAllNodeCanBeReachFromThisNode(nextNodeId, directionEdgeMap, reachableNodeIdSetFromStartNode, reachableNodeIdByPathSetFromStartNode);
}
reachableNodeIdByPathSetFromStartNode.remove(currentNodeId);
return;
}
5 环校验和孤岛节点校验时间复杂度与空间复杂度计算
-
- 约束条件
- 1.1 起始节点个数为1
- 1.2 结束节点个数为b (b > 1)
- 1.3 总结点数量为n(n >= b + 1)
- 1.4 两个点之间最多有一条路径
- 1.5 没有重复的点
- 1.6 没有重复的边
-
- 最多有边
-
2.1 红色圈圈为起始节点,黄色圈圈为结束节点,黑色圈圈为中间节点
-
2.2 当前点的数量为n,当最多边为S(n),观察上图不难发现:
有递推公式S(n + 1) = S(n) + n, 初值 S(b + 1) = b , 约束条件n >= b + 1
有S(n) = n - 1 + S(n - 1)
= (n - 1) + (n - 2) + S(n - 2)
= (n - 1) + (n - 2) + … + (n -(n - b - 1)) + S(n - (n - b - 1))
= (n - 1) + (n - 2) + … + (n -(n - b - 1)) + b
= b + ((n - 1) + n -(n - b - 1)) * ((n - b - 1) - 1 + 1) / 2
= b + (n + b) * (n - b - 1) / 2 -
- 最多有path(起点到终点路径)
-
3.1 红色圈圈为起始节点,黄色圈圈为结束节点,黑色圈圈为中间节点
-
3.2 当前点的数量为n,最多path为S(n),观察上图不难发现:
有递推公式S(n + 1) = S(n) + S(n - 1) + S(n - 2) + … + S(n - (n - b - 1)) + b 初值S(b + 1) = b 约束条件n >= b + 1
则s(n) = S(n - 1) + S(n - 2) + … + S(n - (n - b - 1)) + b
有S(n + 1) - S(n) = S(n)
有S(n + 1) = 2 * S(n)
有S(n) = 2 * S(n - 1)
= (2 ^ (n - b - 1)) * S(n - ( n - b - 1))
= (2 ^ (n - b - 1)) * S(b + 1)
= (2 ^ (n - b - 1)) * b -
- 复杂度
-
4.1 时间复杂度为:o(2 ^ (n - b - 1))(需要遍历所有的path查看有没有环)
-
4.2 空间复杂度为:o(n - b + 1)(起点路过了所有的非起点和终点的点到达了某个终点)
6 广度遍历与深度遍历
- 6.1 深度遍历 :遍历时,先遍历自己,然后递归遍历第一个子节点,再,递归遍历第二个子节点。具体实现请参考环校验
private void validateCycleAndFindAllNodeCanBeReachFromThisNode(Long currentNodeId, Map<Long, Set<Long>> directionEdgeMap, Set<Long> reachableNodeIdSetFromStartNode, Set<Long> reachableNodeIdByPathSetFromStartNode) {
if (reachableNodeIdByPathSetFromStartNode.contains(currentNodeId)) {
throw new IllegalArgumentException("有环");
}
reachableNodeIdSetFromStartNode.add(currentNodeId);
reachableNodeIdByPathSetFromStartNode.add(currentNodeId);
Set<Long> nextNodeSet = directionEdgeMap.get(currentNodeId);
if (null == nextNodeSet || nextNodeSet.size() == 0) {
reachableNodeIdByPathSetFromStartNode.remove(currentNodeId);
return;
}
for (Long nextNodeId : nextNodeSet) {
validateCycleAndFindAllNodeCanBeReachFromThisNode(nextNodeId, directionEdgeMap, reachableNodeIdSetFromStartNode, reachableNodeIdByPathSetFromStartNode);
}
reachableNodeIdByPathSetFromStartNode.remove(currentNodeId);
return;
}
- 6.2 广度遍历:遍历时,先遍历自己,然后遍历自己的所有子节点,再,遍历自己所有子节点的子节点。代码实现:(因为业务需要:这里支持环和孤岛节点情况)
private List<Node> changeNodePositionByDeepInTheGraph(Set<Node> nodeSet, Map<Long, Set<Long>> directionConditionEdgeMap) {
List<Node> oldNodeList = new ArrayList<Node>();
for (Node node : nodeSet) {
oldNodeList.add(node);
}
List<Node> newNodeList = new ArrayList<Node>();
List<Long> nextNodeIdList = new ArrayList<Long>();
nextNodeIdList.add(getStartNodeId());
while (nextNodeIdList.size() > 0) {
List<Long> nextNextNodeIdList = new ArrayList<Long>();
for (Long nextNodeId : nextNodeIdList) {
if (Constant.NO_NEXT_NODE_ID.equals(nextNodeId)) {
continue;
}
Integer nextNodeLocation = findNextNodeLocationInOldNodeList(nextNodeId, oldNodeList);
if (null == nextNodeLocation) {
continue;
}
Set<Long> thisNodeNextNodeIdSet = directionConditionEdgeMap.get(nextNodeId);
newNodeList.add(oldNodeList.get(nextNodeLocation));
oldNodeList.remove((int) nextNodeLocation);
if (null != thisNodeNextNodeIdSet) {
for (Long nextNextNodeId : thisNodeNextNodeIdSet) {
nextNextNodeIdList.add(nextNextNodeId);
}
}
}
nextNodeIdList = nextNextNodeIdList;
}
if (oldNodeList.size() > 0) {
for (int i = 0; i < oldNodeList.size(); i++) {
newNodeList.add(oldNodeList.get(i));
}
}
return newNodeList;
}
private Integer findNextNodeLocationInOldNodeList(Long nextNodeId, List<Node> oldNodeList) {
for (int i = 0; i < oldNodeList.size(); i++) {
if (nextNodeId.equals(oldNodeList.get(i).getId())) {
return i;
}
}
return null;
}
7 最终代码
package xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx;
import java.util.*;
public class Graph {
/**
* 起点
*/
Long startNodeId;
/**
* 所有点的集合
*/
Set<Node> nodeSet;
/**
* 所有边的集合
*/
Set<DirectionConditionEdge> directionConditionEdgeSet;
public Long getStartNodeId() {
return startNodeId;
}
public void setStartNodeId(Long startNodeId) {
this.startNodeId = startNodeId;
}
public Set<Node> getNodeSet() {
return nodeSet;
}
public void setNodeSet(Set<Node> nodeSet) {
this.nodeSet = nodeSet;
}
public Set<DirectionConditionEdge> getDirectionConditionEdgeSet() {
return directionConditionEdgeSet;
}
public void setDirectionConditionEdgeSet(Set<DirectionConditionEdge> directionConditionEdgeSet) {
this.directionConditionEdgeSet = directionConditionEdgeSet;
}
/**
* 节点
*/
private static class Node {
/**
* id
*/
Long id;
/**
* 下一个节点的集合
*/
Map<String, Long> nextNodeIdMap;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Map<String, Long> getNextNodeIdMap() {
return nextNodeIdMap;
}
public void setNextNodeIdMap(Map<String, Long> nextNodeIdMap) {
this.nextNodeIdMap = nextNodeIdMap;
}
}
/**
* 有向边
*/
private static class DirectionConditionEdge {
/**
* 起点
*/
Long fromNodeId;
/**
* 终点
*/
Long toNodeId;
/**
* 条件
*/
String condition;
public Long getFromNodeId() {
return fromNodeId;
}
public void setFromNodeId(Long fromNodeId) {
this.fromNodeId = fromNodeId;
}
public Long getToNodeId() {
return toNodeId;
}
public void setToNodeId(Long toNodeId) {
this.toNodeId = toNodeId;
}
public String getCondition() {
return condition;
}
public void setCondition(String condition) {
this.condition = condition;
}
}
public static class Constant {
public static final String NO_CONDITION = "";
public static final Long NO_NEXT_NODE_ID = -1L;
public static final String TRUE_CONDITION = "true";
public static final String FALSE_CONDITION = "false";
}
public static void main(String[] args) {
Graph graph = buildTestGraph();
// 1 重复点校验
Set<Long> nodeIdSet = graph.validateDuplicatedNode(graph.getNodeSet());
// 2 重复边校验
Map<Long, Set<Long>> directionConditionEdgeMap = graph.validateDuplicatedDirectionConditionEdge(graph.getDirectionConditionEdgeSet());
// 3 环校验
Set<Long> reachableNodeIdSetFromStartNode = new HashSet<Long>();
Set<Long> reachableNodeIdByPathSetFromStartNode = new HashSet<Long>();
graph.validateCycleAndFindAllNodeCanBeReachFromThisNode(graph.getStartNodeId(), directionConditionEdgeMap, reachableNodeIdSetFromStartNode, reachableNodeIdByPathSetFromStartNode);
// 4 孤岛校验
if (reachableNodeIdSetFromStartNode.contains(Constant.NO_NEXT_NODE_ID)) {
nodeIdSet.add(Constant.NO_NEXT_NODE_ID);
}
if (reachableNodeIdSetFromStartNode.size() < nodeIdSet.size()) {
throw new RuntimeException("有孤岛节点");
}
// 5 节点的深度排序
graph.changeNodePositionByDeepInTheGraph(graph.getNodeSet(), directionConditionEdgeMap);
}
private static Graph buildTestGraph() {
Node node1 = new Node();
node1.setId(1L);
Map<String, Long> nextNodeIdMap1 = new HashMap<String, Long>();
nextNodeIdMap1.put(Constant.TRUE_CONDITION, 2L);
nextNodeIdMap1.put(Constant.FALSE_CONDITION, 3L);
node1.setNextNodeIdMap(nextNodeIdMap1);
Node node2 = new Node();
node2.setId(2L);
Map<String, Long> nextNodeIdMap2 = new HashMap<String, Long>();
nextNodeIdMap2.put(Constant.NO_CONDITION, Constant.NO_NEXT_NODE_ID);
node2.setNextNodeIdMap(nextNodeIdMap2);
Node node3 = new Node();
node3.setId(3L);
Map<String, Long> nextNodeIdMap3 = new HashMap<String, Long>();
nextNodeIdMap3.put(Constant.TRUE_CONDITION, Constant.NO_NEXT_NODE_ID);
nextNodeIdMap3.put(Constant.FALSE_CONDITION, 2L);
node3.setNextNodeIdMap(nextNodeIdMap3);
DirectionConditionEdge directionConditionEdge1 = new DirectionConditionEdge();
directionConditionEdge1.setFromNodeId(1L);
directionConditionEdge1.setToNodeId(2L);
directionConditionEdge1.setCondition(Constant.TRUE_CONDITION);
DirectionConditionEdge directionConditionEdge2 = new DirectionConditionEdge();
directionConditionEdge2.setFromNodeId(1L);
directionConditionEdge2.setToNodeId(3L);
directionConditionEdge2.setCondition(Constant.FALSE_CONDITION);
DirectionConditionEdge directionConditionEdge3 = new DirectionConditionEdge();
directionConditionEdge3.setFromNodeId(3L);
directionConditionEdge3.setToNodeId(2L);
directionConditionEdge3.setCondition(Constant.FALSE_CONDITION);
DirectionConditionEdge directionConditionEdge4 = new DirectionConditionEdge();
directionConditionEdge4.setFromNodeId(3L);
directionConditionEdge4.setToNodeId(Constant.NO_NEXT_NODE_ID);
directionConditionEdge4.setCondition(Constant.TRUE_CONDITION);
Set<Node> nodeSet = new HashSet<Node>();
nodeSet.add(node1);
nodeSet.add(node2);
nodeSet.add(node3);
Set<DirectionConditionEdge> directionConditionEdgeSet = new HashSet<DirectionConditionEdge>();
directionConditionEdgeSet.add(directionConditionEdge1);
directionConditionEdgeSet.add(directionConditionEdge2);
directionConditionEdgeSet.add(directionConditionEdge3);
directionConditionEdgeSet.add(directionConditionEdge4);
Graph graph = new Graph();
graph.setNodeSet(nodeSet);
graph.setDirectionConditionEdgeSet(directionConditionEdgeSet);
graph.setStartNodeId(1L);
return graph;
}
private Map<Long, Set<Long>> validateDuplicatedDirectionConditionEdge(Set<DirectionConditionEdge> directionConditionEdgeSet) {
if (null == directionConditionEdgeSet) {
throw new IllegalArgumentException();
}
Map<Long, Set<Long>> directionConditionEdgeMap = new HashMap<Long, Set<Long>>();
for (DirectionConditionEdge directionConditionEdge : directionConditionEdgeSet) {
if (directionConditionEdge.getFromNodeId() == directionConditionEdge.getToNodeId()) {
throw new RuntimeException("有指向自己的边");
}
Set<Long> toNodeIdSet = directionConditionEdgeMap.get(directionConditionEdge.getFromNodeId());
if (null == toNodeIdSet || toNodeIdSet.size() == 0) {
toNodeIdSet = new HashSet<Long>();
toNodeIdSet.add(directionConditionEdge.getToNodeId());
directionConditionEdgeMap.put(directionConditionEdge.getFromNodeId(), toNodeIdSet);
continue;
}
if (toNodeIdSet.contains(directionConditionEdge.getToNodeId())) {
throw new RuntimeException("有重复边");
}
toNodeIdSet.add(directionConditionEdge.getToNodeId());
}
return directionConditionEdgeMap;
}
private Set<Long> validateDuplicatedNode(Set<Node> nodeSet) {
if (null == nodeSet) {
throw new IllegalArgumentException();
}
Set<Long> nodeIdSet = new HashSet<Long>();
for (Node node : nodeSet) {
if (nodeIdSet.contains(node.getId())) {
throw new RuntimeException("有重复点");
}
nodeIdSet.add(node.getId());
}
return nodeIdSet;
}
private void validateCycleAndFindAllNodeCanBeReachFromThisNode(Long currentNodeId, Map<Long, Set<Long>> directionEdgeMap, Set<Long> reachableNodeIdSetFromStartNode, Set<Long> reachableNodeIdByPathSetFromStartNode) {
if (reachableNodeIdByPathSetFromStartNode.contains(currentNodeId)) {
throw new IllegalArgumentException("有环");
}
reachableNodeIdSetFromStartNode.add(currentNodeId);
reachableNodeIdByPathSetFromStartNode.add(currentNodeId);
Set<Long> nextNodeSet = directionEdgeMap.get(currentNodeId);
if (null == nextNodeSet || nextNodeSet.size() == 0) {
reachableNodeIdByPathSetFromStartNode.remove(currentNodeId);
return;
}
for (Long nextNodeId : nextNodeSet) {
validateCycleAndFindAllNodeCanBeReachFromThisNode(nextNodeId, directionEdgeMap, reachableNodeIdSetFromStartNode, reachableNodeIdByPathSetFromStartNode);
}
reachableNodeIdByPathSetFromStartNode.remove(currentNodeId);
return;
}
private List<Node> changeNodePositionByDeepInTheGraph(Set<Node> nodeSet, Map<Long, Set<Long>> directionConditionEdgeMap) {
List<Node> oldNodeList = new ArrayList<Node>();
for (Node node : nodeSet) {
oldNodeList.add(node);
}
List<Node> newNodeList = new ArrayList<Node>();
List<Long> nextNodeIdList = new ArrayList<Long>();
nextNodeIdList.add(getStartNodeId());
while (nextNodeIdList.size() > 0) {
List<Long> nextNextNodeIdList = new ArrayList<Long>();
for (Long nextNodeId : nextNodeIdList) {
if (Constant.NO_NEXT_NODE_ID.equals(nextNodeId)) {
continue;
}
Integer nextNodeLocation = findNextNodeLocationInOldNodeList(nextNodeId, oldNodeList);
if (null == nextNodeLocation) {
continue;
}
Set<Long> thisNodeNextNodeIdSet = directionConditionEdgeMap.get(nextNodeId);
newNodeList.add(oldNodeList.get(nextNodeLocation));
oldNodeList.remove((int) nextNodeLocation);
if (null != thisNodeNextNodeIdSet) {
for (Long nextNextNodeId : thisNodeNextNodeIdSet) {
nextNextNodeIdList.add(nextNextNodeId);
}
}
}
nextNodeIdList = nextNextNodeIdList;
}
if (oldNodeList.size() > 0) {
for (int i = 0; i < oldNodeList.size(); i++) {
newNodeList.add(oldNodeList.get(i));
}
}
return newNodeList;
}
private Integer findNextNodeLocationInOldNodeList(Long nextNodeId, List<Node> oldNodeList) {
for (int i = 0; i < oldNodeList.size(); i++) {
if (nextNodeId.equals(oldNodeList.get(i).getId())) {
return i;
}
}
return null;
}
}