查找有向图中的死循环算法

近期项目遇到一个检测环的问题,记录一波。

大概的需求,前端用户可以输入n个节点,节点之前为有向线段连接,数据从一个叶子节点(只有输出边线)不断向它的连线指向的节点计算,直到这个图算完,如下图

其中叶子节点(节点1)为起始节点,它不停地向前计算,直到算完算到末端节点(节点4及节点7)

这里产生一个问题,如果用户输入的图是有环的,那这个环内就是个死循环,将产生严重后果,所以不允许用户如何输入,那如何检测这个环呢?

算法一:深度遍历

这个算法是最容易想到的,就是把路径都遍历一遍,当其中一条路径检测到重复的节点时,就说明这条路径进入了死循环。

如上图,从末端节点开始,找出所有的路径

1:找出末端节点 4和7

2:从末端节点开始遍历,比如从4开始

    4 -> 2 ok

    7 -> 5 -> 3 -> 1  ok

    7 -> 5 -> 3 -> 2 -> 6 -> 3... NG  3重复出现说明有环 计算结束

代码实现:


 public static List<Long> checkLoopWithTraversal(List<EleLine> lines){
        //找出所有终止结点
        List<Long> finalIds = getFinalIds(lines);
        //整理结点关系图,构建结点对象
        Map<Long, Ele> allEleMap = new HashMap<Long, Ele>();
        for(EleLine line: lines) {
            Ele toEle = allEleMap.get(line.getToEleId());
            if (toEle == null) {
                toEle = new Ele(line.getToEleId());
                allEleMap.put(toEle.getId(), toEle);
            }
            Ele fromEle = allEleMap.get(line.getFromEleId());
            if (fromEle == null) {
                fromEle = new Ele(line.getFromEleId());
                allEleMap.put(fromEle.getId(), fromEle);
            }
            toEle.getFromEleList().add(fromEle);
        }
        for(Long id: finalIds) {
            //定义set 存放某一条路径的所有id
            Set<Long> pathIds = new HashSet<Long>();
            //从终止结点出发遍历
            Long resultEleId = checkLoopEle(pathIds, allEleMap.get(id));
            if (resultEleId != null) {
                //有问问题
                return new ArrayList<Long>(pathIds);
            }
        }
        return null;
    }

   /**
     * 嵌套递归调用
     * @param pathIds
     * @param currentEle
     * @return
     */
    private static Long checkLoopEle(Set<Long> pathIds, Ele currentEle) {
        if (pathIds.contains(currentEle.getId())) {
            return currentEle.getId();
        }
        if (currentEle.getFromEleList().size() == 0) {
            //路径到底,无循环
            return null;
        }
        Long result;
        pathIds.add(currentEle.getId());
        for (Ele ele : currentEle.getFromEleList()) {
            result = checkLoopEle(pathIds, ele);
            if (result != null) {
                return result;
            }
        }
        //该结点下所有子结点检查完毕无循环,将该结点从路径id集合删除
        pathIds.remove(currentEle.getId());
        return null;
    }



算法二:拓扑剥离

假设有100个结点,每为10层,每一次10个结点,相邻的两层的各个结点均与另一层有10个连线,这张图如果把上面的算法去查找,则遍历总张图至少需要遍历10的10次方,这个对cup的占用是可怕的,所以我们研究  另一种法。


如果图里面存在终止结点,那边终止结点肯定不可能在死循环里,所以我们可以把这此终止结点都清除得到新的图,新的图重复上面的动作,最终得到一张没有终止结点的图,或者没有结点的图,前者就是存在死循环,后者就是不存在的。回上文章开头的例子,其循环过程如下:

    图:1 2 3 4 5 6 7

    图:1 2 3 5 6

    图:1 2 3 6 

    剩下的图没有终止结点了,说明有死循环。

代码实现


 private static boolean checkLoopWithTopology(List<EleLine> lines) {
        List<EleLine> checkLines = new ArrayList<EleLine>(lines);
        do {
            int size = checkLines.size();
            removeFromOnlyEle(checkLines);
            if (size == checkLines.size()) {
                return false;
            }
        } while (checkLines.size() > 0);
        return true;
    }

    /**
     * 移除所有的终止结点
     * @param checkLines
     */
    private static void removeFromOnlyEle(List<EleLine> checkLines) {
        Set<Long> fromIds = new HashSet<Long>();
        for (EleLine line : checkLines) {
            fromIds.add(line.getFromEleId());
        }
        for (int i=0; i<checkLines.size(); i++) {
            EleLine line = checkLines.get(i);
            if (!fromIds.contains(line.getFromEleId()) || !fromIds.contains(line.getToEleId())) {
                checkLines.remove(i);
                i--;
            }
        }
    }

再看这个100个结点的例子,用这种算法,只需10次循环就可解出答案。

完整代码参考githup: https://github.com/Kavli/lesson


评论列表

    回复

    提示:您的邮箱只作博主联系您使用,不会显示在评论区. *为必填项