algorithm-cyclefinding
17. 环检测算法-弗洛伊德的兔子和乌龟
简介
环检测应该是一个非常常见的算法问题,怎么判断是否有环的问题呢?
一个很简单的做法就是用HashSet来保存要遍历的数据,如果出现了重复就知道这个链表是有环的。但是这个方法需要保存遍历过的所有的元素,所以其空间复杂度是o(n)。
有没有什么方法可以不用保存之前的元素也能够判断是否有环呢?
来看看弗洛伊德的兔子和乌龟算法吧。
弗洛伊德简介
有的同学会说了,弗洛伊德(Sigmund Freud)谁不知道,梦的解析的作者,大名鼎鼎的心理学专家,精神分析学派的创始人。
但是这里我们讲的弗洛伊德全名是Robert W.Floyd(罗伯特·弗洛伊德)而不是Sigmund Freud。
罗伯特·弗洛伊德是计算机科学家,图灵奖得主,前后断言法的创始人,堆排序算法和Floyd-Warshall算法的创始人之一。
它获得了1978年图灵奖,是一位“自学成才的计算机科学家”。
这个兔子和乌龟算法就是他发明的一种环检测算法。
构造一个环
在学习环状检测之前,我们先要够造一个环。程序是数学和计算机的优美结合。我们是不是可以考虑用数学的方法来构造一个环状结构呢?
假如我们用f(x)来表示这个函数,链表中的每一个元素都是调用f(x)的结果,并且我们将前一个节点的值作为下一个节点f(x)的输入,最后我们将会得到一个下面的集合:
{x0, x1 = f(x0), x2 = f(x1), ..., xi = f(xi-1), ...}
假设f(x)是一个一元二次方程,并且最后的结果对于特定的M取模,那么从x1 到 xi他们的取值范围肯定是在0-M之间。
一直重复下去,肯定会出现一个环形的结构。也就是说一定会出现 xi=xn。
这样我们的环状函数就出来了:
假如我们的初始化输入是62,那么我们将会得到下面的一个环状结构:
环的检测和入口定位
一般来说,我们需要找到环的入口点和环的长度。在弗洛伊德的兔子和乌龟算法中,我们假设乌龟和兔子同时出发,兔子的速度是乌龟的两倍。
也就是说对于初始位置为x0来说,乌龟的下一个位置是f(x0),而兔子的下一个位置是f(f(x0))。
因为兔子的速度是乌龟的两倍,如果链表出现环的情况下,兔子一定会追上乌龟。
这样我们通过判断兔子和乌龟的值是否相等,就可以判断整个链接是否有环。
我们来看一个直观的动画,其中蓝色的点表示的是乌龟,橙色的表示兔子:
上面的动画做了三个事情:
判断是否有环
找到环的入口
找到环的长度
对于的java代码如下:
接下来我们分别一一来讨论
判断是否有环
判断是否有环很简单,我们只需要不断的比较兔子和乌龟的值即可:
找到环的入口
当乌龟和兔子相遇之后,怎么找到环的入口呢?
先来看一个相遇的示意图:
假设在相应点的时候,乌龟行走的距离是x+y,那么对应的兔子行走的距离就是x+y+nL,其中L=y+z是环的长度。
因为兔子的速度是乌龟的两倍,所以我们可以得到下面的等式:
2(x+y)= x +y + nL
简化得到x+y = nL 。
将y 替换成为L-z,得到 x= (n-1)L + z 。
这个结论有么作用呢?
假如这个时候乌龟继续从相遇的节点出发,而兔子改成从初始节点出发,并且速度变成和乌龟一致。
那么当兔子走了x的距离的时候,乌龟刚好走了(n-1)L + z 的距离,也就是说兔子和乌龟将会在环的入口相遇。
我们的代码如下:
找到环的长度
第三步是找到环的长度,这个很简单,我们让乌龟不动,兔子继续前行,等到再次相遇,走过的步数就是环的长度了。
本算法的空间复杂度是O(1),而时间复杂度是O(n)。
本文的代码地址:
本文收录于 www.flydean.com
最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!
欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!
最后更新于