跳到主要内容

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。

这样我们的环状函数就出来了:

    //环路生成函数
//以这样的形式来生成数据:{x0, x1 = f(x0), x2 = f(x1), ..., xi = f(xi-1), ...}
public static int f(int x){
return (3*x*x+7*x+5)%97;
}

假如我们的初始化输入是62,那么我们将会得到下面的一个环状结构:

环的检测和入口定位

一般来说,我们需要找到环的入口点和环的长度。在弗洛伊德的兔子和乌龟算法中,我们假设乌龟和兔子同时出发,兔子的速度是乌龟的两倍。

也就是说对于初始位置为x0来说,乌龟的下一个位置是f(x0),而兔子的下一个位置是f(f(x0))。

因为兔子的速度是乌龟的两倍,如果链表出现环的情况下,兔子一定会追上乌龟。

这样我们通过判断兔子和乌龟的值是否相等,就可以判断整个链接是否有环。

我们来看一个直观的动画,其中蓝色的点表示的是乌龟,橙色的表示兔子:

上面的动画做了三个事情:

  1. 判断是否有环
  2. 找到环的入口
  3. 找到环的长度

对于的java代码如下:

public class CycleFinding {

public static int entryStep;
public static int entryPoint;
public static int cycleLongth;

//环路生成函数
//以这样的形式来生成数据:{x0, x1 = f(x0), x2 = f(x1), ..., xi = f(xi-1), ...}
public static int f(int x){
return (3*x*x+7*x+5)%97;
}

//弗洛伊德兔子乌龟-环检测算法
public static void floydCycleFinding(int x){
//第一步:让乌龟和兔子相遇
// 我们定义两个值,一个是乌龟=f(x),一个是兔子,因为兔子的速度两倍于乌龟,所以我们可以用f(f(x))来表示兔子的值。
int tortoise = f(x), hare = f(f(x));
//然后乌龟和兔子一直向前走,直到他们的值相等,表明两者相遇了
//注意,相遇点并不是环的入口点
while (tortoise != hare) {
tortoise = f(tortoise);
hare = f(f(hare));
}
//第二步:找到环的入口点
//让兔子重新从起点开始起跑,步长和乌龟一致,而乌龟继续原来的位置向后走,当两者相遇的点,就是环的入口点
entryStep = 0; hare = x;
while (tortoise != hare) {
tortoise = f(tortoise);
hare = f(hare); entryStep++;
}
entryPoint= tortoise;

//第三步:找到环的步长
//让兔子继续向前走,乌龟不动,当两者再次相遇的时候,就找到了步长
cycleLongth = 1; hare = f(tortoise);
while (tortoise != hare) {
hare = f(hare); cycleLongth++;
}
}

public static void main(String[] args) {
floydCycleFinding(62);
System.out.printf("entryPoint: %d,cycleLongth %d\n", entryPoint, cycleLongth);
}
}

接下来我们分别一一来讨论

判断是否有环

判断是否有环很简单,我们只需要不断的比较兔子和乌龟的值即可:

//第一步:让乌龟和兔子相遇
// 我们定义两个值,一个是乌龟=f(x),一个是兔子,因为兔子的速度两倍于乌龟,所以我们可以用f(f(x))来表示兔子的值。
int tortoise = f(x), hare = f(f(x));
//然后乌龟和兔子一直向前走,直到他们的值相等,表明两者相遇了
//注意,相遇点并不是环的入口点
while (tortoise != hare) {
tortoise = f(tortoise);
hare = f(f(hare));
}

找到环的入口

当乌龟和兔子相遇之后,怎么找到环的入口呢?

先来看一个相遇的示意图:

假设在相应点的时候,乌龟行走的距离是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 的距离,也就是说兔子和乌龟将会在环的入口相遇。

我们的代码如下:

        //第二步:找到环的入口点
//让兔子重新从起点开始起跑,步长和乌龟一致,而乌龟继续原来的位置向后走,当两者相遇的点,就是环的入口点
entryStep = 0; hare = x;
while (tortoise != hare) {
tortoise = f(tortoise);
hare = f(hare); entryStep++;
}
entryPoint= tortoise;

找到环的长度

第三步是找到环的长度,这个很简单,我们让乌龟不动,兔子继续前行,等到再次相遇,走过的步数就是环的长度了。

        //第三步:找到环的步长
//让兔子继续向前走,乌龟不动,当两者再次相遇的时候,就找到了步长
cycleLongth = 1; hare = f(tortoise);
while (tortoise != hare) {
hare = f(hare); cycleLongth++;
}

本算法的空间复杂度是O(1),而时间复杂度是O(n)。

本文的代码地址:

learn-algorithm

本文收录于 www.flydean.com

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!


点我查看更多精彩内容:www.flydean.com关注公众号加我好友
Loading Comments...