首页 > 其他分享 >拓扑排序实现循环依赖判断

拓扑排序实现循环依赖判断

时间:2023-12-11 16:01:15浏览次数:35  
标签:依赖 int 拓扑 邻接矩阵 source 顶点 排序

本文记录如何通过拓扑排序,实现循环依赖判断

前言

一般提到循环依赖,首先想到的就是Spring框架提供的Bean的循环依赖检测,相关文档可参考:

https://blog.csdn.net/cristianoxm/article/details/113246104

本文方案脱离Spring Bean的管理,通过算法实现的方式,完成对象循环依赖的判断,涉及的知识点包括:邻接矩阵图、拓扑排序、循环依赖。本文会着重讲解技术实现,具体算法原理不再复述

概念释义

1. 什么是邻接矩阵?

这里要总结的邻接矩阵是关于图的邻接矩阵;图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图;一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息;

图分为有向图和无向图,其对应的邻接矩阵也不相同,无向图的邻接矩阵是一个对称矩阵,就是一个对称的二位数组,a[i][j] = a[j][i];
邻接矩阵可以清楚的知道图的任意两个顶点是否有边;方便计算任意顶点的度(包括有向图的出度和入度);可以直观的看出任意顶点的邻接点;

本案例中,有向邻接矩阵图为进行拓扑排序的必要条件之一,其次为有向邻接矩阵图每个顶点的入度

2. 邻接矩阵的存储结构?

vexs[MAXVEX]这是顶点表;

arc[MAXVEX][MAXVEX]这是邻接矩阵图,也是存储每条边信息的二维数组。数组的索引是边的两个顶点,数组的数据是边的权值;

numVertexes, numEdges分别为图的顶点数和边数。

3. 有向邻接矩阵图顶点的入度?

在有向图中,箭头是具有方向的,从一个顶点指向另一个顶点,这样一来,每个顶点被指向的箭头个数,就是它的入度。从这个顶点指出去的箭头个数,就是它的出度

邻接矩阵的行号即代表箭头的出发结点,列号是箭头的指向结点,所以矩阵中同一行为1的表示有从第i个结点指向第j个结点这样一条边,而在同列为1就代表第j个结点被第i个结点指向,因此要求顶点的入度或出度,只需要判断同列为1的个数或同行为1的个数

4. 什么是拓扑排序?

拓扑排序的要素:
1.有向无环图;
2.序列里的每一个点只能出现一次;
3.任何一对 u 和 v ,u 总在 v 之前(这里的两个字母分别表示的是一条线段的两个端点,u 表示起点,v 表示终点);

根据拓扑排序的要素,可通过其有向无环来判断对象依赖是否存在循环。若对象组成的图可完成拓扑排序,则该对象图不存在环,即对象间不存在循环依赖。

拓扑排序除了通过有向邻接矩阵图实现外,还可以通过深度优先搜索(DFS)来实现。本案例中仅讲解前者。

5. 什么是循环依赖?

简单解释如下,若存在两个对象,若A创建需要B,B创建需要A,这两个对象间互相依赖,就构成了最简单的循环依赖关系。

编程示例

1. 对象实体

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ToString
public class RelationVo implements Serializable {

    /**
     * 唯一标识
     */
    private String uniqueKey;

    /**
     * 关联唯一标识集合
     */
    private List combinedUniqueKeys;

}


2. 对象集合转换为有向邻接矩阵图

    /**
     * 将List集合转换为邻接矩阵图的二维数组形式
     *
     * @param sourceList
     * @return
     */
    public static int[][] listToAdjacencyMatrixDiagram(List sourceList) {

        List distinctRelationVoList = new ArrayList(sourceList);
        List keyCollect = distinctRelationVoList.stream().map(RelationVo::getUniqueKey).collect(Collectors.toList());

        for (RelationVo vo : sourceList) {
            vo.getCombinedUniqueKeys().forEach(child -> {
                if (!keyCollect.contains(child)) {
                    // 若叶子节点不在集合中,补充List集合中单独叶子节点,目的是完成提供邻接矩阵图计算的入参
                    keyCollect.add(child);
                    RelationVo build = RelationVo.builder().uniqueKey(child).build();
                    distinctRelationVoList.add(build);
                }
            });
        }

        // 顶点数:对象中出现的全部元素总数
        int vertexNum = keyCollect.size();
        /*
         * 初始化邻接矩阵图的边的二维数组,1表示有边 0表示无边 权重均为1
         * 其中数组下标为边的两个顶点,数组值为对象边的权值(权值=是否有边*权重)
         */
        int[][] edges = new int[vertexNum][vertexNum];

        // 计算邻接矩阵图
        for (int i = 0; i < vertexNum; i++) {
            RelationVo colVo = distinctRelationVoList.get(i);
            List colUniqueKeys = colVo.getCombinedUniqueKeys();
            for (int j = 0; j < vertexNum; j++) {
                RelationVo rowVo = distinctRelationVoList.get(j);
                String rowVertex = rowVo.getUniqueKey();
                if (CollUtil.isNotEmpty(colUniqueKeys)) {
                    if (colUniqueKeys.contains(rowVertex)) {
                        edges[i][j] = 1;
                    } else {
                        edges[i][j] = 0;
                    }
                }
            }
        }
        return edges;
    }
     

3. 计算邻接矩阵图顶点的入度

     /**
     * 返回给出图每个顶点的入度值
     *
     * @param adjMatrix 给出图的邻接矩阵值
     * @return
     */
    public static int[] getSource(int[][] adjMatrix) {
        int len = adjMatrix[0].length;
        int[] source = new int[len];
        for (int i = 0; i < len; i++) {
            // 若邻接矩阵中第i列含有m个1,则在该列的节点就包含m个入度,即source[i] = m
            int count = 0;
            for (int j = 0; j < len; j++) {
                if (adjMatrix[j][i] == 1) {
                    count++;
                }
            }
            source[i] = count;
        }
        return source;
    }
    

4. 对邻接矩阵图进行拓扑排序

    /**
     * 拓扑排序,返回给出图的拓扑排序序列
     * 拓扑排序基本思想:
     * 方法1:基于减治法:寻找图中入度为0的顶点作为即将遍历的顶点,遍历完后,将此顶点从图中删除
     * 若结果集长度等于图的顶点数,说明无环;若小于图的顶点数,说明存在环
     *
     * @param adjMatrix 给出图的邻接矩阵值
     * @param source    给出图的每个顶点的入度值
     * @return
     */
    public static List topologySort(int[][] adjMatrix, int[] source) {
        // 给出图的顶点个数
        int len = source.length;
        // 定义最终返回路径字符数组
        List result = new ArrayList(len);

        // 获取入度为0的顶点下标
        int vertexFound = findInDegreeZero(source);

        while (vertexFound != -1) {
            result.add(vertexFound);
            // 代表第i个顶点已被遍历
            source[vertexFound] = -1;
            for (int j = 0; j < adjMatrix[0].length; j++) {
                if (adjMatrix[vertexFound][j] == 1) {
                    // 第j个顶点的入度减1
                    source[j] -= 1;
                }
            }
            vertexFound = findInDegreeZero(source);

        }
        //输出拓扑排序的结果
        return result;

    }

    /**
     * 找到入度为0的点,如果存在入度为0的点,则返回这个点;如果不存在,则返回-1
     *
     * @param source 给出图的每个顶点的入度值
     * @return
     */
    public static int findInDegreeZero(int[] source) {
        for (int i = 0; i < source.length; i++) {
            if (source[i] == 0) {
                return i;
            }
        }
        return -1;
    }
     

5. 检查集合是否存在循环依赖

    /**
     * 检查集合是否存在循环依赖
     *
     * @param itemList
     */
    public static void checkCircularDependency(List itemList) throws Exception {
        if (CollUtil.isEmpty(itemList)) {
            return;
        }
        // 计算邻接矩阵图的二维数组
        int[][] edges = listToAdjacencyMatrixDiagram(itemList);
        // 计算邻接矩阵图每个顶点的入度值
        int[] source = getSource(edges);
        // 拓扑排序得到拓扑序列
        List topologySort = topologySort(edges, source);
        if (source.length == topologySort.size()) {
            // 无循环依赖
            return;
        } else {
            // 序列集合与顶点集合大小不一致,存在循环依赖
            throw new Exception("当前险种关系信息存在循环依赖,请检查");
        }
    }


单测用例

1. 测试物料-无循环依赖

示例JSON Array结构(可完成拓扑排序):
[{
    "uniqueKey":"A",
    "combinedUniqueKeys":[
        "C",
        "D",
        "E"
    ]
},
{
    "uniqueKey":"B",
    "combinedUniqueKeys":[
        "D",
        "E"
    ]
},
{
    "uniqueKey":"D",
    "combinedUniqueKeys":[
        "C"
    ]
}
]



2. 测试物料-存在循环依赖

示例JSON Array结构(不可完成拓扑排序):
[{
    "uniqueKey":"A",
    "combinedUniqueKeys":[
        "C",
        "D",
        "E"
    ]
},
{
    "uniqueKey":"B",
    "combinedUniqueKeys":[
        "D",
        "E"
    ]
},
{
    "uniqueKey":"D",
    "combinedUniqueKeys":[
        "C"
    ]
},
{
    "uniqueKey":"C",
    "combinedUniqueKeys":[
        "B"
    ]
}
]

3. 单测示例

@Slf4j
public class CircularDependencyTest {
    /**
     * 针对集合信息判断该集合是否存在循环依赖
     */
    @Test
    void testCircularDependencyList() throws Exception {
        String paramInfo = "[{\"uniqueKey\":\"A\",\"combinedUniqueKeys\":[\"C\",\"D\",\"E\"]},{\"uniqueKey\":\"B\",\"combinedUniqueKeys\":[\"D\",\"E\"]},{\"uniqueKey\":\"D\",\"combinedUniqueKeys\":[\"C\"]}]";
        // 序列化
        List list = JSONArray.parseArray(paramInfo, RelationVo.class);
        TopologicalSortingUtil.checkCircularDependency(list);
    }
}


作者:京东保险 侯亚东

来源:京东云开发者社区 转载请注明来源

标签:依赖,int,拓扑,邻接矩阵,source,顶点,排序
From: https://www.cnblogs.com/Jcloud/p/17893780.html

相关文章

  • 排序合并区间
    题目合并区间以数组intervals表示若干个区间的集合,其中单个区间为intervals[i]=[starti,endi]。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。示例1:输入:intervals=[[1,3],[2,6],[8,10],[15,18]]输出:[[1,6],[8,10],[15......
  • MySQL 分组排序后 → 如何取前N条或倒数N条
    开心一刻晚上,老婆辅导女儿写语文作业填空题:春天来了,__绿了女儿:春天来了,爸爸绿了老婆一脸不悦地问道:你再说一遍,春天来了,什么玩意绿了?女儿:春天来了,爸爸绿了呀老婆很生气,但依旧温柔地问道:春天来了,爸爸怎么绿了呢女儿略带哭腔的说道:那冬天呢......
  • 【算法】【线性表】两个排序数组的中位数
    1 题目两个排序的数组A和B分别含有m和n个数,找到两个排序数组的中位数,要求时间复杂度应为O(log(m+n))。中位数的定义:这里的中位数等同于数学定义里的中位数。中位数是排序后数组的中间值。如果有数组中有n个数且n是奇数,则中位数为 A((n-1)/2)。如果有数组中有n个数且n......
  • 算法之快速排序5非递归实现
    一:概述绝大多数的递归逻辑都可以利用栈的方式去代替。代码中一层一层的方法调用,本身就是使用一个方法调用栈。每次进入一个新的方法,就相当于入栈。每次有方法返回就相当于出栈。所以,可以把原本的递归实现转换成一个栈的实现,在栈中存储每一次方法调用的参数。二:具体代码实现/*非......
  • 插入排序详解
    算法思想把数列分成两部分,前面部分为有序区,后面部分为无序区,初始时有序区只有一个元素,一个数字组成的数列当然是有序的;遍历无序区,把其中每个数不断地插入有序区,形成一个更大的有序区,遍历完成时整个数列也就有序了!学习过程思想(1)两层for循环,第一层for循环是无序区,第......
  • 【算法】【线性表】搜索旋转排序数组(有重复数据)
    1 题目跟进“搜索旋转排序数组”,假如有重复元素又将如何?是否会影响运行时间复杂度?如何影响?为何会影响?写出一个函数判断给定的目标值是否出现在数组中。样例1:输入:A=[]target=1输出:false 解释:数组为空,1不在数组中。样例2:输入:A=[3,4,4,5,7,0,1,2]t......
  • linux的sort排序功能
    环境centos7.9sort介绍Linux中的sort功能是一个非常实用的工具,它可以对文本文件进行排序。sort命令可以根据用户指定的规则对文本文件中的行进行排序,并将结果输出到标准输出或指定的文件中简单使用语法sort[选项][文件名]其中,选项可以是以下之一:-r:逆序排序(默认为升序)-n......
  • 详解十大经典排序算法(六):快速排序(QuickSort)
    算法原理分区(Partition):选择一个基准元素,将数组分为两个子数组,小于基准的放在左边,大于基2准的放在右边。递归排序:对左右两个子数组分别进行快速排序。合并:不需要实际的合并操作,因为在分解和递归排序阶段已经完成了排序。算法描述快速排序是一种基于分治思想的高效排序算法,由英国......
  • Day3——揭秘Spring依赖注入和SpEL表达式
    【摘要】在本文中,我们深入探讨了Spring框架中的属性注入技术,包括setter注入、构造器注入、注解式属性注入,以及使用SpEL表达式进行属性注入。我们通过XML和注解两种方式,详细讲解了如何进行属性注入,并给出了完整的代码示例。无论你是Spring新手,还是有一定经验的开发者,本文都将帮助你......
  • Python算法——快速排序
    快速排序(QuickSort)是一种高效的分治排序算法,它选择一个基准元素,将数组分成两个子数组,小于基准的放在左边,大于基准的放在右边,然后递归地排序子数组。快速排序通常比冒泡排序和选择排序更高效,特别适用于大型数据集。本文将详细介绍快速排序的工作原理和Python实现。快速排序的工作原......