用C#语言进行图形中的周期检测
图是最通用的数据结构之一。这是因为它们允许我们解决有趣的问题。它们被用于社交网络和GPS应用中。
人们可以在任何你想对一堆对象之间的关系进行建模的地方应用它。在这篇文章中,我们主要关注的是有循环的图。了解这个概念对帮助我们检测计算机程序中的无限循环很重要。
前提条件
为了能够很好地学习这篇文章,人们需要。
- 安装有[Visual Studio]。
- 对[递归]有一定的了解。
- 对如何使用[邻接列表]和矩阵构建图有一定了解。
- 对C#或任何面向对象的编程语言有基本了解。
主要收获
- 简要介绍一下图。
- 理解图中的循环是什么。
- 理解如何检测一个周期。
- 深度优先搜索算法。
- 有向图上的周期检测。
- 无向图上的周期检测。
- 理解周期检测的不同应用。
图的简要概述
图就像一棵树,但没有任何周期。我们在图中没有一个根节点。下面是一个有四个节点或顶点和六条边或线的图的例子。我们没有限制从一个节点可以有多少个连接。
如果两个节点是相连的,我们就说它们是相邻的或邻居。John 和bob 是邻居。John 和Sam 不是,因为它们没有连接。

如果边有一个方向,我们就说我们有一个有向图。从下面这个有向图的图片来看,John 与Bob 是相连的,但反过来就不是了。这就是Twitter的工作方式。如果你关注某人,就有一个从你的账户到他们账户的连接。而不是相反,除非他们也关注你。

也有不定向的图。这方面的一个例子可以是Facebook。当你添加一个朋友时,有一个从你到他们的连接。这句话是对的。这些连接没有方向。
边缘也可以有权重。我们用这些权重来代表连接的强度,例如,在Facebook上,当两个人交流时,我们可以给他们加更多的权重。然后我们利用这一点,用权重最高的节点来显示他们最好的朋友。

通过这个简单的描述,我们可以更好地理解什么是循环,以及如何在图中检测到它。
什么是图中的循环?
图中的循环是指第一个和最后一个顶点是相同的。如果从一个顶点出发,沿着一条路径行进,最后到达起始顶点,那么这条路径就是一个循环。
循环检测是寻找一个循环的过程。
在我们下面的例子中,我们的路径1 到3 到4 ,再回到1 ,这就是一个循环。在图的上部没有循环。这些是路径1 到2 到6 和1 到2 到7 。在任何搜索中,如果你知道有可能出现循环,你需要管理它。
当这一点没有得到管理时,你的算法将无限次地运行。这将导致StackOverflow 异常错误。

一个关于无限循环如何发生的例子
让我们查一下节点6 。如果我们开始搜索,通过1 到3 到4 的路径,我们将回到1 。在这一点上,我们已经检测到一个循环。没有办法结束它。这个循环将继续下去。因此,我们将永远无法到达6 。
如何检测一个循环
要检测一个图中的循环,深度优先搜索算法是最好的算法。
深度优先搜索
深度优先搜索(DFS),是一种图的遍历方法。我们从一个特定的顶点开始搜索。然后我们探索所有其他的顶点,只要我们能沿着这个路径走。当到达该路径的终点时,我们做一个回溯,直到我们开始的那一点。
在进行回溯时,堆栈数据结构是最好的。

DFS如何工作的例子
从上面的DFS图中,假设1 是我们的起始节点。我们看一下将出现在我们的邻接矩阵中的第一个项目。那就是2 。我们不排队在1 旁边的节点,而是排队在2 旁边的节点。所以我们将去6 。
如果6 是我们要找的节点,我们就停止。让我们假设它不是。我们回溯到2 。从2 ,我们寻找与它相连的其他节点。这就是7 。由于7 没有任何其他连接的节点,而且它也不是我们要找的东西。然后我们回溯到2 。我们已经完成了对所有与2 连接的节点的访问。因此我们回溯到1 。
从1 ,我们检查还有哪个节点与它相连。我们有3 。我们访问节点3 并检查与它相连的节点。我们有8 。我们访问8 。它是最后一个节点。没有其他节点与它相连。如果它不是我们要找的,我们回溯到3 。3 没有其他节点与它相连。我们回溯到1 。
我们再次从我们的列表中检查是否有任何其他节点连接到1 。我们有4 。我们访问4 。4 没有任何其他节点与之相连。我们回溯到1 。1 仍然有5 与它相连。我们访问5 ,并回溯到1 。我们已经完成了对所有节点的访问。
使用DFS实现周期检测
1.有向图中的循环检测
为了检测一个图中的循环,我们访问该节点,将其标记为已访问。然后访问通过它连接的所有节点。当访问一个被标记为已访问并属于当前路径的节点时,将检测到一个周期。

以下是对如何检测周期的解释,以上面的周期图为参考。
我们声明两个布尔数组变量。一个被称为visited。另一个被称为路径。两个数组的大小都是顶点的数量。在这种情况下,7。路径变量是关键。这是因为它将帮助我们确定我们是否找到了一个循环。
这两个布尔数组变量首先被初始化为false。当我们访问每个节点时,我们将访问的变量标记为真。我们也将路径变量标记为真。
- 访问节点
1。把它标记为已访问。把它标记为我们当前路径的一部分。调用节点2。 - 访问节点
2。把它标记为已访问。把它标记为我们当前路径的一部分。调用节点5。 - 访问节点
5。把它标记为已访问。把它标记为我们当前路径的一部分。没有节点与它相连。把它从我们的当前路径中删除,在这个点/索引的路径变量上标记为false。将visited标记为true。返回到节点2。 - 访问节点
6。把它标记为已访问。把它标记为我们当前路径的一部分。节点6,没有子节点。从我们当前的路径中移除它。保留它为已访问的真。 - 从节点
2返回到节点1。从我们的当前路径中删除节点2。这是因为我们已经完成了对所有与它相连的节点的访问。
我们已经访问了节点1,2,5,6 ,只有节点1 是当前路径的一部分。
请注意,所有这些节点都被标记为已访问。
- 从节点
1,转到节点3,把它标记为已访问的节点,并且是我们当前路径的一部分。调用其子节点4。 - 访问节点
4,将其标记为已访问并属于我们当前路径的一部分。调用其子节点1。
在这一点上,我们发现1 是我们当前路径的一部分。它也被标记为被访问。一个循环被检测出来了。
用C#实现循环检测的代码
- 打开Visual Studio。创建一个名为
GraphCycleDetect的控制台应用程序。 - 在
Main()方法中,我们创建一个锯齿状数组graph。这将是容纳我们的顶点并显示它们是如何连接的。这就是我们将用来构建我们的邻接矩阵。它将是图形的一个代表。 - 我们调用
MakeGraph()函数。将顶点的数量和graph作为参数传给该函数。下面是实现步骤1到3的代码。
public static void Main(string[] args)
{
// Created the jagged array. It contains the vertexes and how they are to be connected.
// E.g. new int[]{ 1,2}, means 1 is to be connected to 2
int[][] graph = new int[][]
{
new int[]{ 1,2},
new int[]{ 1,3},
new int[]{ 1,4},
new int[]{ 1,5},
new int[]{ 2,6},
new int[]{ 2,7},
new int[]{ 3,4},
new int[]{ 4,1},
};
// this is the total number of nodes in our graph
int nodes = 8;
// we call the MakeGraph method. This is the method that will constract the graph for us.
var hasCycle = MakeGraph(graph, nodes);
// This will print for us the result on the output window. If we find a cycle it will print:
//Does the graph have a cycle ? ---> true
// If the graph does not have a cycle it will print
//Does the graph have a cycle ? ---> false
Console.WriteLine($"Does the graph have a cycle ? ---> {hasCycle}");
// A built-in c# Static method that reads the next line of character.
//The result will stay on the output window when we add this line.
Console.ReadLine();
}
- 在
MakeGraph()函数中构建邻接矩阵。
- 声明一个字典数据结构,
ls。这将存储我们的邻接列表/图。 - 声明一个被访问的布尔变量,
visited。它的大小是节点的数量。 - 声明一个路径布尔变量,
path。它的大小是节点的数量。 - 用节点的值来填充字典。
- 遍历节点。对于每个迭代。
- 调用
DFS()函数。 - 如果
MakeGraph()函数返回一个true,那么就检测到了一个循环。否则,当它返回false,就没有循环。
下面是上面第4步的代码实现。
private static bool MakeGraph(int[][] graph, int nodes)
{
// This is the dictionary for storing the adjacency list.
//It is of the type, int that will hold a node and List<int> that will hold all other nodes
//attached to the int node.
/* E.G. This is how our graph will look like
1-> 2,3,4
2-> 6,7
3-> 4
4-> 1
*/
Dictionary<int, List<int>> ls = new Dictionary<int, List<int>>();
// We declare a visited bool array variable. We will store the visited nodes in it.
bool[] visited = new bool[nodes];
// We declare a path bool array variable. We will store all nodes in our current path here.
bool[] path = new bool[nodes];
// Loop through our jagged array, graph.
for (int i = 0; i < graph.Length; i++)
{
// As we loop, check whether our dictionary already contains the node at index[i][0]
// of our jagged array, graph. If it is not there, we add it to the dictionary, ls
if (!ls.ContainsKey(graph[i][0]))
{
ls.Add(graph[i][0], new List<int>());
}
// this line of code will connect the nodes. E.g. If we are given { 1,2}, we added 1 to our dictionary
// on the line ls.Add(graph[i][0], new List<int>());
//Therefore, in this next line,ls[graph[i][0]].Add(graph[i][1]); we connect the 1 to the 2
ls[graph[i][0]].Add(graph[i][1]);
}
// We start our traversal here. We could also say that this is where we start our path from.
for (int i = 0; i < nodes; i++)
{
// We do our Dfs starting from the node at i in this case our start point will be 0;
// For each Dfs, we are checking if we will find a cycle. If yes, we immediately return true.
// A cycle has been found.
if (Dfs(ls, i, visited, path))
return true;
}
// If in our for loop above, we never found a cycle, then we will return false.
// A cycle was not detected.
return false;
}
- 创建一个布尔函数
DFS(),检测是否有一个周期。下面是同样的代码。我们使用递归进行回溯。
如果检测到一个循环,我们返回true ,否则,我们返回false 。
private static bool Dfs(Dictionary<int, List<int>> graph, int start, bool[] visited, bool[] path)
{
// If we find that we marked path[start] true, we return true.
// This means that we have come back to the node we started from hence a cycle has
// been found.
if (path[start])
{
return true;
}
// If we didn't find a cycle from the code block above, we mark visited[start] to true.
visited[start] = true;
// We also mark path[start] to true. This will help us know that the node start is on our
// current path.
path[start] = true;
// We check whether our graph contains the start node. Sometimes the start node is not in our graph.
// Thus, if we do our traversal on such a node, an exception will be thrown. This is because the node does
// not exist.
if (graph.ContainsKey(start))
{
// We start our traversal from our start node of the graph.
for each (var item in graph[start])
{
//We do our recursion
// At this point, if the start node returned a true in our recursive call, then we say that cycle has been
// found. We return true immediately.
if (DFS(graph, item, visited, path))
{
return true;
}
}
}
// If we have traversed the whole path from the start node and never found a cycle, we start removing
// those nodes from this path. This is done recursively using c# inbuilt stack also called the call stack.
path[start] = false;
// If we did not find a cycle, we return false.
return false;
}
上述代码运行后的输出将是。

2.无向图中的循环检测。
我们刚才谈到了在有向图中寻找一个周期。在无向图中,方法会有一些不同。
为什么?
下面我们有一个无向图的例子图片。这个图没有方向。这意味着,有一个从A 到B 的连接。还有一个从B 到A 的连接。使用以前的方法,我们从节点A 开始遍历。
我们访问A ,并将其标记为我们当前路径的一部分。然后我们访问与它相连的节点。这就是B 。我们把它标记为已访问并在当前路径集中。因为有一条从B 到A 的边,而且A 是当前路径的一部分。一个循环被检测出来了。
通过上述方法,在每两个由边连接的节点之间总是会检测到一个循环。我们需要为无向图想出一种dfs算法。总是在两条边之间找到一个周期。

正确的DFS算法来检测无定向图中的周期
为了避免在两条边之间找到一个循环,我们要做以下工作。当我们遍历图形时,我们应该将每个节点的父节点传递给其邻居。这将防止返回到父节点。下面是一个详细的解释。
在下图中,我们添加了另一条边C ,形成一个循环。我们从任何一个节点开始我们的深度优先搜索。让我们说A 。我们把A 放在当前路径节点的集合中。然后我们探索A 的邻居。我们可以去B 或C 。顺序并不重要。
我们去B 。B 有两个邻居,A 和C 。我们不想再回到我们来的地方。当我们从A 移动到B ,我们应该通过A 作为父节点。当探索B 的邻居时,我们会知道我们来自A 。我们不会向A 的方向走。我们会去C 。
将C 添加到当前路径集。我们把B 作为父节点。C 还有一个邻居A 。我们已经访问了A 。它也是我们当前路径集的一部分。一个周期被检测出来了。

单向图中循环检测的代码实现
- 打开Visual Studio。创建一个名为
UnidirectedGraphCycleDetect的控制台应用程序。 - 在
Main()方法中,我们创建一个锯齿状数组graph。这将保存我们的顶点并显示它们是如何连接的。 - 我们调用
MakeGraph()函数。将顶点的数量和graph作为参数传给该函数。下面是实现步骤1到3的代码。
public static void Main(string[] args)
{
char[][] graph = new char[][]
{
new char[]{ 'A','B'},
new char[]{ 'B','A'},
new char[]{ 'B','C'},
new char[]{ 'C','B'},
new char[]{ 'C','A'},
new char[]{ 'A','C'},
};
int nodes = 4;
var hasCycle = MakeGraph(graph, nodes);
Console.WriteLine($"Does the graph have a cycle ? ---> {hasCycle}");
Console.ReadLine();
}
- 在
MakeGraph()函数中构建相邻矩阵。这个概念仍然与下面的函数MakeGraph()的有向图的概念相同。区别在于变量的不同。在有向图的代码例子中,我们使用int的值。这里我们使用的是char的值。
private static bool MakeGraph(char[][] graph, int nodes)
{
Dictionary<int, List<char>> ls = new Dictionary<int, List<char>>();
bool[] visited = new bool[100];
bool[] path = new bool[100];
for (int i = 0; i < graph.Length; i++)
{
if (!ls.ContainsKey(graph[i][0]))
{
ls.Add(graph[i][0], new List<char>());
}
ls[graph[i][0]].Add(graph[i][1]);
}
for (int i = 0; i < ls.Count; i++)
{
if (HasCycle(ls, ls.ElementAt(i).Key, visited, -1))
return true;
}
return false;
}
- 我们将在我们现有的类中添加一个新的方法,叫做
HasCycle()。HasCyle(),在这个例子中是Dfs函数。下面是在无向图中实现周期检测的代码。
请注意,在
if (current != parent),我们首先检查我们是否要回到父节点。如果没有,no cycle已经找到了。否则,a cycle is found。
public static bool HasCycle(Dictionary<int, List<char>> connections, int start, bool[] visited, int parent)
{
visited[start] = true;
// Traverse the graph starting from the start node.
foreach (var item in connections[start])
{
if (!visited[item])
{
// This is our recursive call. If in our recursion we find a cycle, we immediately return a true.
if (HasCycle(connections, item, visited, start))
{
return true;
}
}
var current = (int)item;
// This is where the difference comes from the directed graph. As we traverse, we check whether the node is a parent.
// If not then it means that a cycle has been found. We return true.
if (current != parent)
{
return true;
}
}
//If we haven't found a cycle we return false.
return false;
}
上述代码运行后的输出结果将是。

循环检测的应用
- 它经常被用于基于分布式消息的算法中。
- 在并发系统中用于检测死锁。
- 在加密系统中使用。它确定了一个消息的密钥,可以将该相同的消息映射到相同的加密值。
总结
在这篇文章中,我们已经了解了DFS遍历。它是用于检测图中的循环的最佳算法。这是一个重要的概念,特别是当人们发现自己不得不应用它时。我希望这篇文章能使图中周期检测的概念变得清晰。