如何在循环链表中查找循环起始节点?

Posted

技术标签:

【中文标题】如何在循环链表中查找循环起始节点?【英文标题】:How does finding a cycle start node in a cycle linked list work? 【发布时间】:2011-02-25 13:04:54 【问题描述】:

我知道 Tortoise 和 Hare 的会议结束了一个循环的存在,但是如何将 tortoise 移动到链表的开头同时将 hare 保持在会面位置,然后一次移动两个步骤使它们相遇循环的起点?

【问题讨论】:

另一种解释:marcin-chwedczuk.github.io/… 我想知道如果使用布伦特算法,是否有人可以轻松找到循环的开始。 【参考方案1】:

让我试着用我自己的话来澄清Wikipedia - Tortoise_and_hare 提供的循环检测算法。

工作原理

让我们有一只乌龟和一只野兔(指针的名称)用一个循环指向列表的开头,如上图所示。

假设如果我们一次移动乌龟 1 步,一次移动兔子 2 步,它们最终会在一个点相遇。让我们首先证明这个假设是正确的。

该图显示了一个带有循环的列表。循环的长度为n,我们最初与循环的距离为m。另外,假设集合点是距离循环开始的k 步,当乌龟走完i 总步数时,乌龟和野兔相遇。 (到那时,Hare 会采取2i 的总步数。)。

必须满足以下两个条件:

1) i = m + p * n + k

2) 2i = m + q * n + k

第一个说乌龟移动i 步骤,在这些i 步骤中,它首先进入循环。然后它通过循环p 次为某个正数p。最后,它会遍历k 更多节点,直到遇到野兔。

野兔也是如此。它移动2i 步骤,并在这些2i 步骤中首先进入循环。然后它通过循环q 次为某个正数q。最后,它会遍历k 更多节点,直到遇到乌龟。

由于兔子的行进速度是乌龟的两倍,到达交汇点的时间是恒定的。

所以通过使用简单的速度、时间和距离关系,

2 ( m + p * n + k ) = m + q * n + k

=> 2m + 2pn + 2k = m + nq + k 

=>  m + k = ( q - 2p ) n

mnkpq 中,前两个是给定列表的属性。如果我们可以证明至少有一组 kqp 的值使这个等式成立,我们就证明这个假设是正确的。

一个这样的解决方案集如下:

p = 0

q = m

k = m n - m

我们可以验证这些值的工作方式如下:

m + k = ( q - 2p ) n  

=> m + mn - m = ( m - 2*0) n

=> mn = mn

对于这个集合,i

i = m + p n + k

=> m + 0 * n + mn - m = mn

当然,您应该看到这不一定是最小的i。也就是说,龟兔赛跑可能已经见过很多次了。但是,由于我们表明它们至少在某个时间点相遇,因此我们可以说该假设是正确的。因此,如果我们将其中一个移动 1 步,将另一个移动 2 步,它们将不得不相遇。

现在我们可以进入算法的第二部分,即如何找到循环的开始。

循环开始

一旦乌龟和兔子相遇,让我们将乌龟放回列表的开头,并将兔子放在它们相遇的地方(距离循环起点k 几步之遥)。

假设是,如果我们让它们以相同的速度移动(两者都走一步),它们第一次再次相遇将是循环的开始。

让我们证明这个假设。

我们首先假设某个预言机告诉我们m 是什么。

然后,如果我们让它们移动m + k 步,乌龟将不得不到达它们最初遇到的点(k 距离循环开始的步距 - 见图)。

之前我们展示了m + k = (q - 2p) n

由于m + k 步数是循环长度n 的倍数,因此在此期间,野兔将经历循环 (q-2p) 次并返回同一点(k 步距循环开始)。

现在,如果我们让它们只移动m 步,而不是让它们移动m + k 步,乌龟将到达循环开始处。 Hare 距离完成 (q-2p) 旋转还差k 步骤。由于它在循环开始前k 步骤开始,所以兔子必须在循环开始处到达。

因此,这解释了他们必须在第一次经过一定数量的步骤后在循环开始时相遇(这是第一次,因为乌龟在m 步骤之后才到达循环,它永远不会看看已经在循环中的兔子)。

现在我们知道我们需要移动它们直到它们相遇的步数原来是从列表开头到循环开头的距离m。当然,算法不需要知道m是什么。它只会一步一步地移动乌龟和兔子,直到它们相遇。汇合点必须是循环起点,步数必须是到循环起点的距离 (m)。假设我们知道列表的长度,我们还可以计算从列表长度中减去m 的循环长度。

【讨论】:

我不这么认为,当他们相遇时,这就是起点,请参阅下面的评论:***.com/a/19209858/1744146<br>如果我错了,请告诉我 解释的第一部分是完美的。但据我所知,第二部分有一个缺陷。您假设“某个 oracle 说 m”,但如果 m 已知,则您已经有了循环的开始。当您永远不知道周期的开始在哪里时,您怎么能假设答案?请告诉我。 @Gopichand 再次阅读最后一段...您只是假设有一些 m(如果已经证明存在循环)..但是您不知道 m 的值 我想问即使快指针是慢指针的三倍,或者是慢指针的四倍或n倍,慢指针和快指针还是会相遇的,对吧? 你的方程m + k = (q - 2p) n可以进一步简化为m + k = q*n。这是因为乌龟绕圈的次数总是为零,因为兔子永远不会在没有遇到乌龟的情况下超越乌龟。想想吧。【参考方案2】:

参考这张图片:

slowPointer 在相遇前经过的距离 = x + y

fastPointer 在相遇前经过的距离 = (x + y + z) + y = x + 2y + z

因为 fastPointer 以 两倍 的速度行驶,是 slowPointer 的速度,并且到达汇合点时两者的时间是恒定的

所以通过使用简单的速度、时间和距离关系 2(x+y)= x+2y+z => x+2y+z = 2x+2y => x=z

因此,通过将 slowPointer 移动到链表的开头,并使 slowPointer 和 fastPointer 一次移动一个节点,它们都有相同的覆盖距离

它们将到达链表中循环开始的点。

【讨论】:

这里没有考虑fastPointer在slowPointer进入循环之前经过n次循环的情况。用 l 表示循环的长度。 fastPointer 在相遇前经过的距离 = (x + y + z) + y = x + 2y + nl + z。得到的关系将是 x = nl+z。 @JingguoYao:这里是that case.的解释 此图过于简单。快指针可以在慢指针到达之前在循环中移动多次。 @Warren MacEvoy:这张图很简单是件好事。它有助于理解基本情况。下一个案例是here。这种情况同样容易理解。【参考方案3】:

这是Floyd's algorithm for cycle detection。您是在询问算法的第二阶段——一旦您找到一个属于循环的节点,如何找到循环的开始

在弗洛伊德算法的第一部分,兔子每走一步,乌龟就走两步。如果龟兔赛跑,就存在一个循环,相遇点是循环的一部分,但不一定是循环中的第一个节点。

当乌龟和兔子相遇时,我们找到了最小的 i(乌龟走的步数)使得 Xi = X2i。让 mu 表示从 X0 到循环开始的步数,让 lambda 表示循环的长度。然后 i = mu + alambda 和 2i = mu + blambda,其中 a 和 b 是整数,表示龟兔赛跑了多少次。减法 第二个方程的第一个方程给出 i = (b-a)*lambda,所以 i 是整数倍 的拉姆达。 因此,Xi + mu = Xmu。 Xi 表示龟兔赛跑的交汇点。如果将乌龟移回起始节点X0,并让乌龟和兔子以相同的速度继续前进,经过mu个额外的步骤,乌龟将到达Xmu ,而兔子将到达 Xi + mu = Xmu,因此第二个交汇点表示循环的开始。

【讨论】:

@Jim lewis 汇合点当然不是起点,但正如我所说,将其中一个移到链表的开头并以相同的速度移动会使它们在起点相遇循环点。 @Jim Lewis 如果您能解释一下如何将 i 作为循环长度的倍数导致 mu 作为第一个汇合点和循环起点之间的距离,那就太好了。 @Passionate:从起点采取 mu 步到达X_mu,即循环的起点(根据 mu 的定义)。然后,如果您采取更多步骤,其中 i 是循环长度的倍数,您最终会回到循环开始:X_mu + i = X_mu。但是加法是可交换的,所以这相当于从起点采取 i 步到达第一个交汇点X_i,然后再走多步回到X_mu,即循环的起点。 @ankur:汇合点是 X_i,我们已经证明(我的回答中的第三段)i 必须是循环长度的倍数。经过交汇点的额外步骤后,您现在位于 X_(i + mu)。但是我们已经证明了 X_(i + mu) = X_(mu + i) = X_mu,因为 i 的这个特殊属性,所以 mu 步过汇合点必须带你到 X_mu,即循环的开始。基本上是模算术,加上加法的交换性质。 我认为你的证明有一个小问题。由于汇合点i 位于循环的某个点,我认为等式应该是i = mu + k + a*lambda2i = mu + k + b*lambda,其中k 是从循环开始到汇合点的步数。减去这两个方程得到相同的结果。【参考方案4】:

Old Monk's simple and under-upvoted answer 解释了当快跑者只完成一个完整循环时找到循环。在这个答案中,我解释了快跑者在慢跑者进入循环之前多次运行循环的情况。


使用同一张图片:

假设快跑者在慢跑和快跑相遇之前已经运行了 m 次循环。这意味着:

慢跑距离:x + y 快速奔跑的距离:x + m(y + z) + y 即他们相遇的额外y

由于快跑的速度是慢跑的两倍,而且他们跑的时间相同,这意味着如果我们将慢跑的距离加倍,我们就会得到快跑的距离。因此,

2(x + y) = x + m(y + z) + y

求解 x 给出,

x = (m - 1)(y + z) + z

在实际场景中,这意味着,x = (m - 1) 完整的循环运行 + 额外的距离 z

因此,如果我们将一个指针放在列表的开头,而将另一个指针留在集合点,那么以相同的速度移动它们将导致循环内指针完成 m - 1 循环运行,然后在循环开始处遇到另一个指针。

【讨论】:

一个疑问..它如何保证慢速和快速在慢速超过一个周期之前相遇? @siraj:慢速不会循环运行,快速会因为它运行得比慢速快并且会在之前进入循环。并且保证他们会见面。如果慢在 j + 1,快在 j,他们现在将在 j + 2 相遇。如果慢在 j,快在 j + 1,这意味着他们已经在 j - 1 相遇。 如果慢循环,数学仍然有效:x+(y+z)m+y = 2(x+(y+z)n+y),其中 n 是循环的次数在他们相遇之前放慢脚步。这解决了 (m-2n-1)(y+z)+z=x。这意味着从汇合点开始,绕 (m-2n-1) 次,你回到汇合点,然后走 z,你在循环的开始。为此,它与从头节点开始并进入 x 个节点相同。 @mayas_mom:数学可能会奏效,但慢速将永远无法绕过循环。它总是会在开始时或中途某个地方被捕获。 x = (m - 1)(y + z) + z 这可以概括为循环长度为 y+z 并且只关心位置。所以 x = ((m - 1)(y + z))%(y+z)) + z 这实际上是 x=z;【参考方案5】:

非常非常简单。你可以从相对速度的角度来思考。如果兔子移动了两个节点,乌龟移动了一个节点,相对于乌龟,兔子移动了一个节点(假设乌龟处于静止状态)。所以,如果我们在循环链表中移动一个节点,我们肯定会在那个点再次相遇。

在找到循环链表内部的连接点之后,现在问题被简化为找到两个链表的交点问题。

【讨论】:

我非常努力地理解这个答案,但这个说法对我来说毫无意义“如果我们在循环链表中移动一个节点,我们肯定会在那个时候再次相遇"【参考方案6】:

在第一次碰撞时,乌龟移动了 m+k 步,如上所示。兔子的移动速度是乌龟的两倍,这意味着兔子移动了 2(m+k) 步。从这些简单的事实我们可以得出以下图表。

此时,我们将乌龟移回起点,并声明兔子和乌龟都必须一次移动一步。根据定义,在 m 步之后,乌龟将处于循环的开始。兔子会在哪里?

野兔也将在周期的开始。从第二张图中可以清楚地看出:当乌龟回到起点时,兔子 k 步进入了最后一个循环。 m 步后,兔子将完成另一个循环并与乌龟相撞。

【讨论】:

@WarrenMacEvoy 我从来没有建议他们在起点见面。正如数字清楚地指出的那样,他们在周期开始再次相遇。【参考方案7】:

方法:

有两个指针:

一次移动一个节点的慢速指针。 一次移动两个节点的快速指针。

如果两个指针相遇,则证明存在循环。一旦他们相遇,其中一个节点将指向头部,然后两个节点一次都前进一个节点。他们将在循环开始时相遇。

理由: 当两个人沿着圆形轨道行走时,其中一个人的速度是另一个人的两倍,他们在哪里相遇?正是他们开始的地方。

现在,假设跑得快的人在 n 步数圈中领先 k 步。他们会在哪里见面?正好在n-k 步骤。当慢跑者跑过(n-k) 步时,快跑者会跑过k+2(n-k) 步。 (k+2n-2k 步即2n-k)。即(n-k) steps(路径是圆形的,我们不关心他们相遇的轮数;我们只关心他们相遇的位置)。

现在跑得快的人是如何在k 步数中领先一步的呢?因为慢跑者花了很多步才能到达循环的开始。所以循环的开始距离头节点 k 步。

注意:两个指针相遇的节点距离循环起点(循环内)距离 k 步远,头节点也距离循环起点 k 步远。因此,当我们让指针从 bot 这些节点以 1 步的相同速度前进时,它们将在循环开始时相遇。

我相信这很简单。如果有任何不明确的部分,请告诉我。

【讨论】:

请在此处发布完整的答案,而不仅仅是一个将来可能会中断的链接【参考方案8】:

好吧,假设兔子和乌龟在距离循环开始 k 步的点相遇,循环开始前的步数是 mu,循环的长度是 L。

所以现在在集合点 ->

乌龟覆盖的距离 = mu + a*L + k - 公式 1

(到达循环开始所采取的步骤 + 覆盖循环的“a”次迭代所采取的步骤 + 从循环开始的 k 步) (其中 a 是一些正常数)

野兔覆盖的距离 = mu + b*L + k - 公式 2

(到达循环开始所采取的步骤 + 覆盖循环的“b”次迭代所采取的步骤 + 从循环开始的 k 步) (其中 b 是某个正常数,b>=a)

所以野兔所走过的额外距离是 = 公式 2 - 公式 1 = (b-a)*L

请注意,这个距离也等于乌龟到起点的距离,因为兔子的移动速度是乌龟的 2 倍。这可以等同于'mu+k',如果我们不包括循环的多次遍历,这也是会合点到起点的距离。

因此, mu + k = (b-a)*L

因此,从这一点开始的 mu 步将导致回到循环的开始(因为从循环开始的 k 步已经到达汇合点)。这可能发生在同一周期或任何后续周期中。 因此,现在如果我们将乌龟移动到链表的开头,它将需要 mu 步才能到达循环的起点,而兔子也需要 mu 步才能到达循环的起点,因此它们都会在循环的起点。

附:老实说,我的想法与原始海报相同,我阅读了第一个答案,他们确实清除了一些东西,但我无法清楚地得到最终结果,所以我尝试按照自己的方式进行并发现更容易理解。

【讨论】:

他们通常不会在周期开始时相遇【参考方案9】:

将问题简化为循环问题,然后回到最初的问题

我觉得下面的解释更直观。

    取两个从头部 (O) 开始的指针(1 = 乌龟,2 = 兔子),1 的步长为 12 的步长为 2。想想当1到达那个循环的开始节点(A)的那一刻。

    我们要回答以下问题“当 1 在 A 中时 2 在哪里?”

    所以,OA = a 是一个自然数 (a &gt;= 0)。但可以这样写:a = k*n + b,其中a, k, n, b are natural numbers

    n = 周期长度 k &gt;= 0 = 常量 0 &lt;= b &lt;= n-1

    表示b = a % n

    例如:如果a = 20n = 8 => k = 2b = 4 因为20 = 2*8 + 4

    1 覆盖的距离是d = OA = a = k*n + b。但同时,2 覆盖了D = 2*d = d + d = OA + d = OA + k*n + b。这意味着当 2 在 A 中时,它必须覆盖 k*n + b。如您所见,k 是圈数,但是在这些圈之后,2 将是 b 远离 A。所以,我们找到了 2 是当 1 在 A 中时。我们称该点为B,其中AB = b

    现在,我们将问题简化为一个圆圈。问题是“会面点在哪里?”。那个C在哪里?

    在每一步中,2 都会减少与 11(假设是米)的距离,因为 1 距离 1 越来越远strong>2 与 1,但同时 2 更接近 1 2

    所以,当 12 之间的距离为零时,相交将是零。这意味着 2 减少了n - b 距离。为了实现这一点,1 将执行 n - b 步骤,而 2 将执行 2*(n - b) 步骤。

    所以,相交点将是远离A(顺时针)的n - b,因为这是1所经过的距离,直到它遇到2。 => CA 之间的距离是CA = b,因为AC = AB + BC = n - bCA = n - AC。不要以为AC = CA,因为AC距离不是一个微不足道的数学距离,它是AC之间的步数(其中A 是起点,C 是终点)。

    现在,让我们回到最初的架构。

    我们知道a = k*n + bCA = b

    我们可以取两个新指针1'1'',其中1'从头部开始(O) 和 1'' 从交点 (C) 开始。

    1'OA 时,1''C 到 A 并继续完成k 圈。所以,交点是A

【讨论】:

【参考方案10】:

使用relative velocity 的思想进行简单的解释,在高中任教 - 物理 101 / 运动学讲座。

    假设从链表开始到圆开始的距离是x hops。让我们将圆的起点称为点X(大写字母 - 见上图)。另外让我们假设圆圈的总大小是 N 跳。

    兔子的速度 = 2 * 乌龟的速度。所以分别是1 hops/sec2 hops/sec

    当乌龟到达圆圈的起点X时,兔子必须进一步在x处跳到图中的点Y。 (因为兔子的距离是乌龟的两倍)。

    因此,从 X 到 Y 顺时针方向的剩余弧的长度将是 N-x。 T也恰好是兔子和乌龟能够相遇的相对距离。假设这个相对距离将及时覆盖t_m,即见面的时间。相对速度为(2 hops/sec - 1 hops/sec),即1 hops/sec。因此,使用相对距离 = 相对速度 X 时间,我们得到 t = N-x 秒。所以N-x 需要到达乌龟和兔子的交汇点。

    现在在N-x 秒时间和1 hops/sec 速度下,早在X 点的乌龟将经过N-x 跳到达集合点M。因此,这意味着集合点M 位于N-xX 逆时针跳=(这进一步暗示)=> 从点MX 顺时针方向还有x 距离。

    但是x也是从链表起点到X点的距离。

    现在,我们不关心 x 对应的跃点数。如果我们将一只乌龟放在 LinkedList 的开头,一只乌龟放在集合点 M 并让它们跳跃/行走,那么它们将在点 X 相遇,这就是我们需要的点(或节点)。

【讨论】:

【参考方案11】:

如果如图所示指针在点P相遇,那么距离Z+Y就是点P,X+Y也是点P,即Z=X。这就是为什么不断将一个指针从 P 移动并从 start(S) 移动另一个指针直到它们相遇,这意味着将相等的距离(Z 或 X)移动到同一点 M(Z 到 P 的距离和 X 到 S 的距离)将是循环的开始。简单!

【讨论】:

【参考方案12】:

- 在循环之前有 k 步。我们不知道 k 是什么,也不需要找出来。我们可以只用 k 抽象地工作。

--经过k步

----- T 处于循环开始

----- H 是 k 步进入循环(他总共走了 2k 步,因此 k 进入循环)

** 它们现在是循环大小 - k 分开

(注意 k == K == mod(loopsize, k) - 例如,如果一个节点在 5 个节点的循环中分 2 步,那么它也是 7、12 或 392 步,所以这个循环有多大k 不考虑在内。

由于他们以每单位时间 1 步的速度赶上对方,因为一个人的移动速度是另一个人的两倍,他们将在 loopsize - k 处相遇。

这意味着它需要k个节点到达循环的开始,因此从head到cyclestart的距离以及从collision到cyclestart的距离是相同的。

所以现在在第一次碰撞后将 T 移回头部。如果您以 1 的速率移动,T 和 H 将在循环开始时相遇。 (都在 k 步中)

这意味着算法是:

从头部移动 T = t.next 和 H.next.next 直到它们碰撞 (T == H)(有一个循环)

//注意当k=0或T和H在循环的头部相遇时的情况 计算循环的长度

--用计数器移动T或H来计算循环的长度

--将指针T2移动到链表头

--移动循环步长指针长度

--移动另一个指针H2到head

--串联移动T2和H2,直到它们在循环开始时相遇

就是这样!

【讨论】:

【参考方案13】:

image credit

调用距离是指针跟随的链接数,时间是算法将慢速指针移动一个链接和快速指针移动两个链接所花费的迭代次数。在长度为 C 的循环之前有 N 个节点,用循环偏移量 k=0 到 C-1 标记。

要到达循环的开始,慢需要 N 时间和距离。这意味着快速在循环中需要 N 距离(N 到达那里,N 旋转)。所以在时间 N,slow 是在循环偏移 k=0,fast 是在循环偏移 k=N mod C。

如果 N mod C 为零,则慢速和快速现在匹配,并且在时间 N 和循环位置 k=0 处找到循环。

如果 N mod C 不为零,那么现在快必须赶上慢,在时间 N 是循环中落后的 C-(N mod C) 距离。

由于每 1 次慢速快速移动 2 次,因此每次迭代将距离减少 1,这需要与时间 N 时快速和慢速之间的距离一样多的额外时间,即 C-(N mod C)。由于慢是从偏移量 0 开始移动,这也是它们相遇的偏移量。

因此,如果 N mod C 为零,则阶段 1 在循环开始时经过 N 次迭代后停止。否则,阶段 1 在 N+C-(N mod C) 次迭代后在偏移量 C-(N mod C) 处停止。

// C++ pseudocode, end() is one after last element.

int t = 0;
T *fast = begin();
T *slow = begin();
if (fast == end()) return [N=0,C=0];
for (;;) 
    t += 1;
    fast = next(fast);
    if (fast == end()) return [N=(2*t-1),C=0];
    fast = next(fast);
    if (fast == end()) return [N=(2*t),C=0];
    slow = next(slow);
    if (*fast == *slow) break;

好的,所以第 2 阶段:slow 需要 N 多步才能进入循环,此时 fast(现在每个时间步移动 1 个)位于 (C-(N mod C)+N) mod C = 0。所以他们在第 2 阶段之后的周期开始时相遇。

int N = 0;
slow = begin();
for (;;) 
    if (*fast == *slow) break;
    fast = next(fast);
    slow = next(slow);
    N += 1;

为了完整起见,第 3 阶段通过再次移动循环来计算循环长度:

int C = 0;
for (;;) 
    fast = next(fast);
    C += 1;
    if (fast == slow) break;

【讨论】:

链接到谷歌文档以模拟算法:docs.google.com/spreadsheets/d/… 请注意,如果 N 【参考方案14】:

已经有很多答案了,但我曾经为此想出了一个图表,这对我来说在视觉上更直观。也许它可以帮助其他人。

对我来说主要的惊喜时刻是:

T(乌龟)拆分为 T1(循环前)和 T2(循环内)。

H 中减去 T,它们在视觉上是重叠的。剩下的 (H - T = H') 等于 T

剩下的数学很简单。

【讨论】:

【参考方案15】:

通过以上所有分析,如果您是一个通过实例学习的人,我会尝试编写一个简短的分析和示例,以帮助解释其他人试图解释的数学。我们开始吧!

分析:

如果我们有两个指针,一个比另一个快,并一起移动它们,它们最终会再次相遇以表示一个循环或 null 表示没有循环。

要找到循环的起点,让 ...

    m是头部到循环开始的距离;

    d为循环中的节点数;

    p1是较慢指针的速度;

    p2 是更快指针的速度,例如。 2 表示一次通过两个节点。

    观察以下迭代:

 m = 0, d = 10:
 p1 = 1:  0  1  2  3  4  5  6  7  8  9 10 // 0 would the start of the cycle
 p2 = 2:  0  2  4  6  8 10 12 14 16 18 20

 m = 1, d = 10:
 p1 = 1: -1  0  1  2  3  4  5  6  7  8  9
 p2 = 2: -1  1  3  5  7  9 11 13 15 17 19

 m = 2, d = 10:
 p1 = 1: -2 -1  0  1  2  3  4  5  6  7  8
 p2 = 2: -2  0  2  4  6  8 10 12 14 16 18

从上面的示例数据中,我们可以很容易地发现,每当更快和更慢的指针相遇时,它们距离循环开始的距离为m。要解决此问题,请将较快的指针放回头部,并将其速度设置为较慢指针的速度。当他们再次相遇时,节点就是循环的开始。

【讨论】:

【参考方案16】:

让我们说,

N[0] is the node of start of the loop, 
m is the number of steps from beginning to N[0].

我们有 2 个指针 A 和 B,A 以 1x 速度运行,B 以 2x 速度运行,都从头开始。

当 A 到达 N[0] 时,B 应该已经在 N[m] 中。 (注:A 使用 m 步到达 N[0],B 应该再进一步 m 步)

然后,A 再跑 k 步与 B 碰撞, 即 A 在 N[k],B 在 N[m+2k] (注意:B 应该从 N[m] 开始运行 2k 步)

A 分别在 N[k] 和 N[m+2k] 处碰撞 B,这意味着 k=m+2k,因此 k = -m

因此,要从 N[k] 循环回到 N[0],我们还需要 m 个步骤。

简单地说,我们只需要在找到碰撞节点后再运行 m 步即可。我们可以有一个从头开始运行的指针和一个从碰撞节点运行的指针,它们将在 m 步后在 N[0] 处相遇。

因此,伪代码如下:

1) A increase 1 step per loop
2) B increase 2 steps per loop
3) if A & B are the same node, cycle found, then go to 5
4) repeat from 1
5) A reset to head
6) A increase 1 step per loop
7) B increase 1 step per loop
8) if A & B are the same node, start of the cycle found
9) repeat from 6

【讨论】:

【参考方案17】:

我不认为当他们相遇时这就是起点。但是是的,如果另一个指针(F)在之前的交汇点处,则该指针将位于循环的结尾而不是循环的开头,并且从列表开头开始的指针(S)将结束在循环的开始。例如:

1->2->3->4->5->6->7->8->9->10->11->12->13->14->15->16->17->18->19->20->21->22->23->24->8

Meet at :16

Start at :8

public Node meetNodeInLoop()

    Node fast=head;
    Node slow=head;

    fast=fast.next.next;
    slow=slow.next;

    while(fast!=slow)

        fast=fast.next;
        fast=fast.next;

        if(fast==slow) break; 

        slow=slow.next;
    

    return fast;



public Node startOfLoop(Node meet)

    Node slow=head;
    Node fast=meet;

    while(slow!=fast)
        fast=fast.next;
        if(slow==fast.next) break;
        slow=slow.next;
    

    return slow;

【讨论】:

【参考方案18】:

用图表来解决这个问题会有所帮助。我试图在没有方程式的情况下解释这个问题。

    如果我们让兔子和乌龟跑一圈,然后兔子跑两次乌龟,那么在一圈结束时,兔子乌龟会跑到一半。在两圈结束时,兔龟会跑完一圈,它们都会相遇。这适用于所有速度,例如如果兔子跑了 3 次,兔子跑 1 圈等于乌龟的 1/3,所以在 3 圈结束时,兔子乌龟会跑完 1 圈并相遇。 现在,如果我们在循环前 m 步开始它们,则意味着更快的野兔正在循环中提前开始。因此,如果乌龟到达循环的开始,兔子会提前 m 步循环,当它们相遇时,它将在循环开始前 m 步。

【讨论】:

【参考方案19】:

我看到大多数答案都对此给出了数学解释“如何将乌龟移动到链表的开头同时将野兔保持在会面地点,然后一次移动两个步骤使它们在开始时相遇周期点?"

下面的方法在幕后也像弗洛伊德循环检测一样,但原理很简单,但代价是 O(n) 内存。

我想添加一个更简单的方法/理由来找到周期的开始。由于没有在任何地方提及此方法,因此我在这里进行了测试:https://leetcode.com/problems/linked-list-cycle-ii/ 并且它通过了所有测试用例。

假设我们已经获得了 LinkedList 的头部引用。

     public ListNode detectCycle(ListNode head) 
     
            // Consider a fast pointer which hops two nodes at once.
            // Consider a slow pointer which hops one node at once.
            // If the linked list contains a cycle,
            // these two pointers would meet at some point when they are looping around the list.
            // Caution: This point of intersection need not be the beginning of the cycle.
            ListNode fast = null;
            ListNode slow = null;
            if (head != null) 
                if (head.next != null) 
                    fast = head.next.next;
                    slow = head;
                 else 
                    return null;
                
            
            while (fast != null && fast.next != null) 
                // Why do we need collection here? Explained below
                Set<ListNode> collection = new HashSet<>();
                if (fast == slow) 
                  // Once the cycle is detected, 
                     we are sure that there is beginning to the cycle.
                  // In order to find this beginning, 
                  // 1. move slow pointer to head and keep fast pointer at 
                        the meeting point.
                  // 2. now while moving slow and fast pointers through a 
                        single hop, store the slow reference in a collection.
                  // 3. Every time you hop the fast pointer, check the fast 
                        pointer reference exits in that collection.
                  // Rationale: After we moved slow pointer to the head, 
                     we know that slow pointer is coming behind the fast
                     pointer, since collection is storing all nodes from the 
                     start using slow pointer, there is only one case we get 
                     that fast pointer exists in the collection when slow 
                     pointer started storing the nodes which are part of the 
                     cycle. Because slow pointer can never go ahead of fast 
                     pointer since fast pointer already has an head-start, at 
                     the same time, the first occurence will always be of the 
                     starting point of the cycle because slow pointer can't 
                     go ahead of fast pointer to store other nodes in the 
                     cycle. So, the moment we first find fast pointer in that 
                     collection means, that is the starting point of the 
                     cycle.
                    slow = head;
                    collection.add(slow);
                    while (!collection.contains(fast)) 
                        slow = slow.next;
                        collection.add(slow);
                        fast = fast.next;
                    
                    return fast;
                
                fast = fast.next.next;
                slow = slow.next;
            
            return null;
        

【讨论】:

【参考方案20】:

花了两个小时试图阅读所有答案后,我在 leetcode 上找到了这条评论。可以肯定地说,它拯救了我的夜晚。

https://leetcode.com/problems/linked-list-cycle-ii/discuss/44774/Java-O(1)-space-solution-with-detailed-explanation./44281

【讨论】:

【参考方案21】:

我知道这个问题已经有一个公认的答案,但我仍然会尝试以流畅的方式回答。 假设:

The length of the Path is 'X+B' where 'B' is the length of the looped path and X of the non looped path. 
    Speed of tortoise : v
    Speed of hare     : 2*v 
    Point where both meet is at a distance 'x + b - k' from the starting point.

现在,让兔子和乌龟在时间“t”之后相遇。

观察:

如果,乌龟经过的距离 = v*t = x + (b-k)(比方说)

那么,野兔经过的距离 = 2*v*t = x + (b - k) + b(因为野兔已经穿过环形部分一次)

现在,会议时间相同。

=> x + 2*b - k = 2* (x + b - k)

=> x = k

这当然意味着不循环的路径的长度与循环的起点到两者交汇点的距离相同。

【讨论】:

你不能假设乌龟在相遇时正好移动了 x+b-k。另外,我不明白你是如何得到 x+2*b-k 的兔子距离的。 因为兔子已经穿过环形部分必须遇到乌龟..我没有在那里解释:/【参考方案22】:

实际上很容易证明他们都会在起点相遇,如果你考虑到相遇点背后的数学。 首先让m表示链表中循环的起点,n表示循环的长度。那么为了让兔子和乌龟见面,我们有:

( 2*t - m )%n = (t - m) %n, where t = time (at t = 0 , both are at the start)

用数学的方式表述:

(2*t - m - (t - m) ) = 0 modulo n , which implies , t = 0 modulo n 

所以他们会在时间 t 相遇,这应该是 cycle 长度的倍数。这意味着他们在一个地点相遇,即 (t-m) modulo n = (0-m) modulo n = (-m) modulo n.

所以现在回到问题,如果你从链表的开头移动一个指针,从交点移动另一个指针,经过 m 步后,我们将让兔子(它在循环内移动)来到点是((-m) + m) modulo n = 0 modulo n,它只是循环的起点。所以我们可以看到,经过 m 步后,它到达了循环的起点,乌龟会在那里遇到它,因为它会遍历 m 从链表开始的步骤。

顺便说一句,我们也可以这样计算他们相交的时间:条件t = 0 modulo n告诉我们他们将在一个是循环长度的倍数的时间相遇,并且也是t 应该大于 m ,因为它们会在循环中相遇。因此所用时间将等于 n 的第一个倍数,它大于 m

【讨论】:

他们不一定会在周期开始时相遇。【参考方案23】:

假设你的指针在 y 和 z 的交点相遇。

n 和 m 分别是更快和更慢的指针在相遇之前循环的次数。

请参阅图片以获取其余证明。 Find the starting point of loop in linked list

【讨论】:

以上是关于如何在循环链表中查找循环起始节点?的主要内容,如果未能解决你的问题,请参考以下文章

数据结构第五篇——线性表的链式存储之循环链表

数据结构:单向循环链表

数据结构:单向循环链表

制作循环链表并找到循环的开头

数据结构之单向不循环链表

Python 单向循环链表