有向无环图孤岛和环校验以及深度遍历与广度遍历

有向无环图孤岛和环校验以及深度遍历与广度遍历

1 背景

在阿里巴巴做项目时用到的图,这里抽象出来有关算法和数据结构部分,不带有其他业务信息

2 有向无环图的约束

  1. 只有一个起始节点
  2. 有多个结束节点,且结束节点的数量为b
  3. 每个节点不能有到自身的边
  4. 每个节点到另外一个节点最多一条边
  5. 每个边仅有一个条件,或者没有条件,没条件用空字符串表示
  6. 没有重复的边和重复的点
    图的感性认识

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 环校验和孤岛节点校验

    1. 环校验核心思想 :遍历每条path(起始节点到结束节点之间的路径称之为path),查看path中有没有重复的点。如果有重复的点,则有环,没有重复的节点,则没有环。
    1. 孤岛节点校验核心思想:从起始节点出发,根据边,找到所有有可能到达的节点。对比,所有的节点,如果有节点不可到达,则有孤立节点,如果没有节点不可达,则没有孤岛节点。
    1. 校验的具体实现:使用递归和试探回溯算法进行验证。
    • 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中移除当前节点,回到上一个处理的节点
    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;
    }

5 环校验和孤岛节点校验时间复杂度与空间复杂度计算

    1. 约束条件
    • 1.1 起始节点个数为1
    • 1.2 结束节点个数为b (b > 1)
    • 1.3 总结点数量为n(n >= b + 1)
    • 1.4 两个点之间最多有一条路径
    • 1.5 没有重复的点
    • 1.6 没有重复的边
    1. 最多有边
  • 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

    1. 最多有path(起点到终点路径)
  • 3.1 红色圈圈为起始节点,黄色圈圈为结束节点,黑色圈圈为中间节点

  • path变化图

  • 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

    1. 复杂度
  • 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;
    }

}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值