この記事ではトポロジカルソートによる循環依存判定の実装方法を記録します。
序文
一般に、循環依存関係というと、Spring フレームワークによって提供される Bean の循環依存関係の検出が最初に思い浮かびます。関連ドキュメントについては、以下を参照してください。
https://blog.csdn.net/cristianoxm/article/details/113246104
この記事のソリューションは Spring Bean 管理から脱却し、アルゴリズム実装を通じてオブジェクトの循環依存関係の判断を完了します。関係する知識ポイントには、隣接行列グラフ、トポロジカル ソート、循環依存関係が含まれます。この記事では技術的な実装に焦点を当て、特定のアルゴリズムの原則については繰り返しません。
コンセプト説明
1. 隣接行列とは何ですか?
ここでまとめる隣接行列は、グラフに関する隣接行列であり、グラフの隣接行列の格納方法は、グラフを表現するために2つの配列を使用し、1次元配列はグラフ内の頂点情報を格納し、1次元配列はグラフを表現する。 2 次元配列 (隣接行列と呼ばれます) は、グラフ内のエッジまたは円弧に関する情報を格納します。
グラフは有向グラフと無向グラフに分けられ、対応する隣接行列も異なります。無向グラフの隣接行列は対称行列であり、対称な 2 桁の配列 a[i][j] = a[ j ][i];
隣接行列は、グラフの任意の 2 つの頂点にエッジがあるかどうかを明確に知ることができ、任意の頂点の次数 (有向グラフの出次数と入次数を含む) を計算するのに便利です。任意の頂点の隣接点を直観的に確認できます。
この場合、有向隣接行列グラフは位相ソートに必要な条件の 1 つであり、その後に有向隣接行列グラフの各頂点の次数が続きます。
2. 隣接行列の記憶構造は何ですか?
vexs[MAXVEX] これは頂点テーブルです。
arc[MAXVEX][MAXVEX] これは隣接行列グラフであり、各エッジに関する情報を格納する 2 次元配列でもあります。配列のインデックスはエッジの 2 つの頂点であり、配列のデータはエッジの重みです。
numVertexes、numEdges はそれぞれグラフの頂点とエッジの数です。
3. 有向隣接行列グラフの頂点の入次数は何ですか?
有向グラフでは、矢印にはある頂点から別の頂点へ向かう方向があり、各頂点を指す矢印の数がその入次数となります。この頂点から出ている矢印の数がその出次数です
隣接行列の行番号は矢印の始点ノード、列番号は矢印の指すノードを表します。したがって、行列の同じ行に 1 がある場合は、i 番目のノードからエッジがあることを意味します。 j 番目のノードに、同じ列内でこれが 1 の場合、j 番目のノードが i 番目のノードによってポイントされていることを意味します。頂点の場合、同じ列内の 1 の数、または同じ行内の 1 の数を決定するだけで済みます。
4. トポロジカルソートとは何ですか?
トポロジカルソートの要素:
1. 有向非巡回グラフ;
2. シーケンス内の各点は 1 回だけ出現できる;
3. u と v の任意のペアでは、u は常に v の前に来ます (ここでの 2 つの文字はそれぞれ a の 2 つの端点を表します)線分、u は始点を表し、v は終点を表します)。
トポロジカルソートの要素によれば、オブジェクトの依存関係に循環があるかどうかは、その有向非循環性によって判断できます。オブジェクトで構成されるグラフがトポロジカル ソートを完了できる場合、オブジェクト グラフには循環は存在しません。つまり、オブジェクト間に循環依存関係はありません。
トポロジカル ソートは、有向隣接行列グラフを通じて実装されるだけでなく、深さ優先検索 (DFS) を通じて実装することもできます。ここでは前者についてのみ説明する。
5. 循環依存関係とは何ですか?
簡単に説明すると、2 つのオブジェクトがあり、A が B を作成する必要があり、B が A を作成する必要がある場合、これら 2 つのオブジェクトは相互に依存しており、最も単純な循環依存関係が形成されます。
プログラミング例
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);
}
}
IntelliJ IDEA 2023.3 と JetBrains Family Bucket の年次メジャー バージョン アップデート 新しいコンセプト「防御型プログラミング」: 安定した仕事に就く GitHub.com では 1,200 を超える MySQL ホストが稼働していますが、8.0 にシームレスにアップグレードするにはどうすればよいですか? Stephen Chow の Web3 チームは来月、独立したアプリをリリースする予定ですが、 Firefox は廃止されるのでしょうか? Visual Studio Code 1.85 リリース、フローティング ウィンドウ Yu Chengdong: ファーウェイは来年破壊的な製品を発売し、業界の歴史を書き換えるだろう 米国 CISA はメモリ セキュリティの脆弱性を排除するために C/C++ の廃止を勧告 TIOBE 12 月: C# がプログラミングになると予想30年前に 雷軍が書いた論文「コンピュータウイルス判定エキスパートシステムの原理と設計」著者:JD保険侯亜東
出典:JD Cloud Developer Community 転載の際は出典を明記してください