“判断图中是否有环”是一道经常出现在面试中经典的算法题,我们今天就来讲讲这道题的含义和解法,包含Python编码全过程。

题目中的概念

判断图中是否有环这道题目首先涉及到两个概念:图和环。

这里的图就是计算机数据结构中说的图结构(Graph),它包括两个要素:顶点和边,前者又称为节点。

节点表示事物的抽象,而边则表示事物之间两两的联系。

【算法】如何确定图(Graph)里有没有环(Cycle)?_算法

边可以分为有方向和无方向两种。有方向的边表示两个节点之间单向的连通,而无方向的边则表示双向连通。边有方向的图叫做有向图,反之叫做无向图。

【算法】如何确定图(Graph)里有没有环(Cycle)?_队列_02

环则是指在途中一条由边组成的路径,从一个节点出发,可以回到这个节点自身。

【算法】如何确定图(Graph)里有没有环(Cycle)?_队列_03

判断无向图中是否有环

通过上面的定义可知,无论有向图还是无向图中都存在环,但有向图的环涉及到边的方向,要比无向图复杂。

因此,如果你在面试中被要求写一个算法“判断图中是否有环”,首先就应该和面试官确认,要判断的是有向图还是无向图。本文我们讲解的是无向图中是否有环的判断!

比如下面这两个无向图,很显然图一里面有环,而图二没有。

【算法】如何确定图(Graph)里有没有环(Cycle)?_算法_04

从算法的原理开始

用眼睛看起来很简单的事情,如何用程序来实现呢?

在动手编程之前,我们首先要想清楚如何做,也就是说我们先要能够找到一个用自然语言可以描述的办法,来确定无向图中是否有环。

其实很多算法最难的一点实在这里,平白的给你一张无向图,你能找出一个切实可行的办法,把它描述出来,别人只要按照指示去做,就一定能正确地确认任何一个无向图里面有没有环吗?

【算法】如何确定图(Graph)里有没有环(Cycle)?_队列_05

如果你从来没有学过相关的知识,自己拍脑袋就想出这样的一个办法来了!那么恭喜,你已经具备了创造算法的能力!不过对于大多数人来说,我们还是需要寻求前人的帮助。

最简单的方法:在互联网上查找一下。我们在搜索引擎中输入“判断无向图有没有环”这个查询语句,然后看到很多相关的搜索结果。

【算法】如何确定图(Graph)里有没有环(Cycle)?_机器学习_06

我们直接点击第一个。看到了下面这个文章。

【算法】如何确定图(Graph)里有没有环(Cycle)?_数据结构_07

本文中讲的内容比较多,介绍了三种方法:拓扑排序,DFS和Union-Find Set,每一种方法都可以判断无向图或者有向图。

拓扑排序法判断一个无向图中是否有环

“判断一个无向图有没有环”的方法本文中就有三个。这里,我们先取第一种方法:拓扑排序判断无向图是否有环。这种方法的描述如下:

使用拓扑排序可以判断一个无向图中是否存在环,具体步骤如下:

 

    1. 求出图中所有节点的度。

    2. 将所有度 <= 1 的节点入队。

    3. 当队列不空时进入循环,弹出队首元素,把与队首元素相邻节点的度减一。如果相邻节点的度变为1,则将相邻节点入队。队列为空则退出循环。

    4. 循环结束时判断已经访问过(进入过队列)的节点数是否等于 n。等于 n 说明全部节点都被访问过,无环;反之,则有环。

我们将算法改写成控制流程图是下面这样:

【算法】如何确定图(Graph)里有没有环(Cycle)?_机器学习_08

这里面又涉及到了一个概念——节点的度。

什么叫做节点的度呢?其实很简单,节点的度是指和该节点相关联的边的条数。

如果是有向图,还要分入度和出度,不过我们现在要处理的是无向图,所以,每条边都是平等的,统一都记作度数。

【算法】如何确定图(Graph)里有没有环(Cycle)?_队列_09

人肉模拟运行算法

我们来找两个例子,按照算法模拟运行一下。

第一个例子

先看图一,图一中节点1,2,3的度是2,节点4和5的度是3,而节点6和7的度是1。

【算法】如何确定图(Graph)里有没有环(Cycle)?_算法_10

那首先,我们要把节点6和7放到队列里。

然后将节点6弹出,把和节点6相邻的节点5的度减一。从图上,就相当于擦掉了节点5和节点6之间的边。按理说此时节点6的度也应该减掉1,但是因为节点6我们已经处理过,它以后不会再进入队列,我们也不不会再关心它的度,因此也不用去处理它的度了。

此时队列还没有空,所以我们继续弹出队首并处理。弹出节点7,将和它相邻的节点4的度减一,相当于删除了它们之间的边。

现在队列已经弹空,退出了循环。我们再看进入过队列的节点只有2个,而节点一共有7个,2 不等于 7, 所以有环。

第二个例子

现在再来看图二,一共5个节点,相应的度分别是1,2,2,2,1。

【算法】如何确定图(Graph)里有没有环(Cycle)?_python_11

此第一拨入队列的是度为1的节点1和节点5。

节点1出队列后和它相邻的节点2度数减一,也变成了度为1的节点,因此节点2入队列。

再度进入循环,弹出节点5,和它相邻的节点4度数减一后入队列。继续循环弹出节点2,节点3入队列。

循环弹出节点4,节点3的度变成0,不用再度入栈。

最后一次循环弹出节点3,和它相邻的再无度为1的节点,循环结束。

至此,所有节点都已经入过队列,因此可知,无环。

直观来看,算法是有效的。

确定数据结构

那么下面是不是就该编程实现了?稍等,别忘了,程序 = 算法 + 数据结构。我们现在只有算法,还没有描述无向图的数据结构。

图的表示方法不止一种,此处我们采用邻接矩阵表示无向图。很多时候,当面试官出题要求判断图中是否有环时,会特意指出要用邻接矩阵。如果没指出,就是让你自选。

【算法】如何确定图(Graph)里有没有环(Cycle)?_数据结构_12

邻接矩阵是一个 n 阶的方阵,n 为图中顶点个数。方阵中每个元素的值只有两种可能,要么 0 ,要么 1。若第 i 行第 j 列的元素为 1,则说明 i 节点和 j 节点相邻,也就是有一条无向边存在于二者之间,若为 0,则说明节点 i 和 j 不相邻。

由此图一和图二对应的矩阵分别是这样:

【算法】如何确定图(Graph)里有没有环(Cycle)?_python_13

邻接矩阵也可以用在有向图上。

不过对无向图而言:

    i) 邻接矩阵一定是对称的,而且主对角线一定为零(自己不可能和自己相邻)。

    ii) 在无向图中,节点 i 的度是矩阵第 i 行(或第 i 列)所有非零元素的个数。因为非零元素的取值只能是 1,因此节点 i 的度也是邻接矩阵第 i 行所有值的和。

另一方面,方阵就是一个二维表,在程序内部,正好用一个二位数组或列表(List)来表示。

很好,既然如此,我们就可以开始编程了。

编程实现算法

我们用Python来编。

在正式实现算法之前,我们先要进行数据处理,也就是我们需要将表达无向图的矩阵读取到内存中。

这里又涉及到该数据在磁盘存储的问题。我们就用最简单的方式,将邻接矩阵直接存储为 csv 文件,就像这样:

【算法】如何确定图(Graph)里有没有环(Cycle)?_机器学习_14

我们专门定义一个函数(如下图)做数据处理,那么在读取的时候,我们就可以用 Python的csv library,用csv.reader() 读取 csv文件,然后再转化为列表。这个列表就是算法的输入。

【算法】如何确定图(Graph)里有没有环(Cycle)?_队列_15

现在来看算法本身。

我们定义一个函数,名为 is_undirected_graph_circled,它接受一个输入参数:adj_matrix,这个adj_matrix 是一个二维表。

要处理二维表,也就是输入的邻接方阵,我们首先要知道方阵的阶数,那么很好办,我们只要用 len 函数,就可以。

然后我们要计算所有节点的度,并且将度  <=1 的节点压入队列。这里要用到队列,我们就用Python自带的queue library,先import,然后直接创建一个队列。

接着计算每个节点的度,将它们存储在degrees列表里,用一个循环,每个循环对用矩阵的一行,然后 sum函数将该行中所有的元素相加。

这里有一点要注意,我们直接用csv.reader读取出来的数据是字符串,我们要对其进行数据转换,将其转化为整数型,这样才能有效地计算度。

算出一个节点的度后直接判断是否小于等于 1,若是则入队列。

这里还要注意一件事情,我们的算法最终要判断有多少节点入过队,但是队列本身要不断地压入弹出,里面不可能保留所有入过队的节点。所以要用一个专门的列表存储每个入队的元素。就是这个visited。

【算法】如何确定图(Graph)里有没有环(Cycle)?_数据结构_16

做完这些就该进入到最核心的循环部分了。循环中的关键则是:把与队首元素相邻节点的度减 1。

我们该怎么找到与队首节点相邻的节点呢?比如节点 i,在邻接方阵里,第 i 行和第 i 列的所有元素都记录了它的邻居,那么我们可以选取第 i 行作为线索,找到所有值为 1 的元素,该元素所在的列数 j 所对应的 j 节点,就是与 i 相邻的节点。

那么我们需要将degrees里对应 j 元素的值减去 1。然后看看它减掉 1 后的值是否为 1,若是则入队,否则不管。

当队中元素全部弹出后,循环结束,我们看看 visited 列表中的元素个数是否已经达到了 n 个,若是则说明无环,否则有环。

【算法】如何确定图(Graph)里有没有环(Cycle)?_机器学习_17

算法函数定义好之后,可以在数据处理函数中调用,然后把结果打印出来。

完整代码和数据请见: 

https://github.com/juliali/ClassicAlgorithms/tree/main/undirected_graph

 

 

 

【算法】如何确定图(Graph)里有没有环(Cycle)?_算法_18