为啥在链表中查找循环时将指针增加 2,为啥不增加 3、4、5?

Posted

技术标签:

【中文标题】为啥在链表中查找循环时将指针增加 2,为啥不增加 3、4、5?【英文标题】:Why increase pointer by two while finding loop in linked list, why not 3,4,5?为什么在链表中查找循环时将指针增加 2,为什么不增加 3、4、5? 【发布时间】:2011-07-05 01:17:42 【问题描述】:

我已经看过question,它讨论了在链表中查找循环的算法。我已经阅读了Floyd's cycle-finding algorithm 解决方案,在很多地方都提到我们必须采取两个指针。一个指针(slow/tortoise)增加 1,另一个指针(faster/hare)增加 2。当它们相等时,我们找到循环,如果更快的指针达到 null,则链表中没有循环。

现在我的问题是为什么我们将更快的指针增加 2。为什么不做别的?增加 2 是必要的,或者我们可以将其增加 X 以获得结果。如果我们将更快的指针增加 2 是否有必要找到一个循环,或者可能存在需要增加 3 或 5 或 x 的情况。

【问题讨论】:

不幸的是,您链接到的第一篇文章(弗洛伊德算法)是由不太关心教别人如何理解算法的人撰写的。我可以接受该算法有效,但我还没有找到一个很好的 english 描述为什么 它有效。希望这个答案能得到那个描述。 @Lasse 我的情况也是如此,我知道它有效,但不明白这背后的逻辑和原理。 看看Brent's algorithm,反正比较好。 @LasseVågsætherKarlsen 看到这个answer 【参考方案1】:

理论上,将循环(循环)视为公园(圆形,矩形等),第一人称 X 移动缓慢,第二人称 Y 移动速度比 X 快。现在,如果人 Y 移动并不重要速度是 X 的 2 倍或 3,4,5... 倍。他们在某一点相遇总会有一个案例。

【讨论】:

【参考方案2】:

假设我们使用两个引用 Rp 和 Rq,它们在每次迭代中采取 p 和 q 步; p > q。在弗洛伊德算法中,p = 2,q = 1。

我们知道,在某些迭代之后,Rp 和 Rq 都将位于循环的某些元素处。然后,假设 Rp 比 Rq 领先 x 步。也就是说,从Rq的元素开始,我们可以走x步到达Rp的元素。

比如说,循环有 n 个元素。在 t 次进一步迭代之后,Rp 将领先于 Rq (x + (p-q)*t) 步。因此,只有在以下情况下,它们才能在 t 次迭代后相遇:

n 除 (x + (p-q)*t)

可以写成:

(p−q)*t ≡ (−x) (mod n)

由于模运算,这只有在以下情况下才有可能: GCD(p−q, n) | x.

但我们不知道 x。但是,如果 GCD 为 1,它将除以任何 x。将 GCD 设为 1:

如果 n 未知,则选择任意 p 和 q,使得 (p-q) = 1。弗洛伊德算法确实有 p-q = 2-1 = 1。 如果 n 已知,则选择任意 p 和 q,使得 (p-q) 与 n 互质。

更新:在稍后的进一步分析中,我意识到任何不相等的正整数 pq 都会使这两个引用在一些迭代后相遇。不过,1 和 2 的值似乎需要较少的总步数。

【讨论】:

【参考方案3】:

这是一种直观的非数学方式来理解这一点:

如果快速指针跑出链表的末尾,显然没有循环。

忽略指针在列表的初始非循环部分的初始部分,我们只需要让它们进入循环。当慢速指针最终到达循环时,快指针在循环中的哪个位置并不重要。

一旦他们都在循环中,他们就会在循环中循环,但在不同的点。想象一下,如果他们每次都移动一个。然后他们将在循环中转圈,但保持相同的距离。换句话说,制作相同的循环但异相。现在通过将快速指针每一步移动两步,它们就可以相互改变相位;每走一步就将他们之间的距离缩短一个。快指针会赶上慢指针,我们可以检测到循环。

为了证明这是真的,他们会相遇,并且快速指针不会以某种方式超越和跳过慢速指针,只需手动模拟当快速指针落后慢速指针三步时会发生什么,然后模拟当快速指针比慢速指针落后两步,那么当快速指针仅比慢速指针落后一步时。在每种情况下,它们都在同一个节点相遇。任何更大的距离最终都会变成三、二或一的距离。

【讨论】:

虽然这可以解释循环检测,但它只解决了“为什么是 2?”的问题。与 1 相比,而不是 3、4、5 等。在这一点上,虽然这不是一个糟糕的答案,但我认为它实际上并没有回答这个问题。【参考方案4】:

如果有一个循环(n 个节点),那么一旦指针进入循环,它将永远保持在那里;所以我们可以及时向前移动,直到两个指针都在循环中。从这里开始,指针可以用具有初始值 a 和 b 的整数模 n 表示。那么他们经过t步后满足的条件是

a+t≡b+2t mod n 有解 t=a−b mod n。

只要速度之间的差异与 n 不共享主要因素,这将起作用。

参考 https://math.stackexchange.com/questions/412876/proof-of-the-2-pointer-method-for-finding-a-linked-list-loop

对速度的唯一限制是它们的差异应该与循环的长度互质。

【讨论】:

【参考方案5】:

假设不包含循环的列表长度为s,循环长度为tfast_pointer_speedslow_pointer_speed 的比率为k

让两个指针在距离循环起点j 处相遇。

所以,慢速指针移动的距离 = s + j。快速指针行进的距离 = s + j + m * t(其中 m 是快速指针完成循环的次数)。但是,快速指针也会移动一段距离k * (s + j)k 乘以慢速指针的距离)。

因此,我们得到k * (s + j) = s + j + m * t

s + j = (m / k-1)t.

因此,根据上述等式,慢速指针所经过的长度是循环长度的整数倍。

为了获得最大效率,(m / k-1) = 1(慢速指针不应该多次遍历循环。)

因此,m = k - 1 => k = m + 1

由于m 是快速指针完成循环的次数,m >= 1。 为了获得最大的效率,m = 1

因此k = 2.

如果我们取 k > 2 的值,则两个指针必须行进的距离越远。

希望以上解释对您有所帮助。

【讨论】:

@Sumit:如果您采用指针速度的比率,那么较慢的人也可能不止一次地遍历循环,因此较慢的人行进的距离可能不仅仅是 s+j。可以说较慢的一者移动 2 步,而较快的一者移动 5 步。我错过了什么吗? 是的。确实如此 。如果您采用 2 的比率,则较短的指针不需要多次遍历循环,因此是最佳的。这就是我试图证明的。正如您所指出的,其他比率不是最佳的,较短的指针可能会多次遍历循环。 你能说出为什么在这个等式中:s + j = (m / k-1)t , (m/k-1) 一定是整数吗? 谢谢,这终于为我阐明了算法。【参考方案6】:

如果快指针移动3 步,慢指针移动1 步,则不能保证两个指针在包含偶数个节点的循环中相遇。但是,如果慢速指针以2 步长移动,则会议将得到保证。

一般来说,如果野兔以H步移动,而乌龟以T步移动,则保证您在一个循环中遇到H = T + 1

假设兔子相对于乌龟移动。

兔子相对于乌龟的速度是H - T 每次迭代的节点。

给定一个长度为N =(H - T) * k 的循环,其中k 是任何正数 整数,野兔会跳过每个H - T - 1 节点(再次,相对 到乌龟),他们不可能见面,如果 乌龟在这些节点中的任何一个中。

保证会面的唯一可能是H - T - 1 = 0

因此,允许将快指针增加x,只要慢指针增加x - 1

【讨论】:

【参考方案7】:

如果链表有一个循环,那么增量为 2 的快速指针会比增量为 3 或 4 或更多的指针工作得更好,因为它确保一旦我们进入循环内部,指针肯定会发生冲突并且不会有超车。

例如,如果我们以 3 为增量,并且在循环内假设

fast pointer --> i  
slow         --> i+1 
the next iteration
fast pointer --> i+3  
slow         --> i+2

而这种情况永远不会以 2 为增量发生。

另外,如果你真的不走运,那么你最终可能会遇到循环长度为L 并且你将快速指针增加L+1 的情况。然后你会被无限卡住,因为移动快慢指针的差异总是L

我希望我说清楚了。

【讨论】:

即使循环长度为L,也可以将快速指针增加L+1。它每次都会在同一个地方结束,但这不是问题,因为慢速指针会抓住它。 @j_random_hacker .... 慢指针怎么能赶上快指针??两者之间的差异将始终保持不变......因为这就像两者都增加了 1。 忍不住对这个老帖子发表评论 :) 它们相互捕捉的方式与秒针和分针最终在钟面上相遇的方式相同。【参考方案8】:

没有理由需要使用第二个。任何步长选择都可以(当然,除了一个)。

要了解为什么会这样,让我们​​先看看为什么 Floyd 的算法会起作用。这个想法是考虑序列 x0, x1, x2, ..., xn, ... 如果您从列表的开头开始,然后继续向下走直到到达末尾,您将访问的链接列表的元素。如果列表不包含循环,则所有这些值都是不同的。但是,如果它确实包含一个循环,那么这个序列将无限重复。

这是使弗洛伊德算法起作用的定理:

链表包含一个循环当且仅当存在一个正整数 j 使得对于任何正整数 k,xj = xjk

让我们去证明这一点;这并不难。对于“if”情况,如果存在这样的 j,则选择 k = 2。那么对于某些正 j,xj = x2j 和 j ≠ 2j,所以列表包含一个循环。

对于另一个方向,假设列表包含从位置 s 开始的长度为 l 的循环。令 j 是 l 大于 s 的最小倍数。那么对于任意k,如果我们考虑xj和xjk,由于j是循环长度的倍数,我们可以认为xjk 作为从列表中的位置 j 开始,然后采取 j 步 k-1 次形成的元素。但是每次你走 j 步,你就会回到你在列表中开始的地方,因为 j 是循环长度的倍数。因此,xj = xjk.

这个证明向您保证,如果您在每次迭代中采取任何恒定数量的步数,您确实会遇到慢速指针。更准确地说,如果您在每次迭代中采取 k 步,那么您最终会找到点 xj 和 xkj 并检测到循环。直觉上,人们倾向于选择 k = 2 来最小化运行时间,因为您在每次迭代中采取的步数最少。

我们可以更正式地分析运行时如下。如果列表不包含循环,则快速指针将在 n 步后到达列表末尾 O(n) 时间,其中 n 是列表中元素的数量。否则,两个指针会在慢速指针经过 j 步后相遇。请记住,j 是 l 大于 s 的最小倍数。如果 s ≤ l,则 j = l;否则,如果 s > l,则 j 最多为 2s,因此 j 的值为 O(s + l)。由于 l 和 s 不能大于列表中元素的数量,这意味着 j = O(n)。然而,在慢速指针已经走了 j 步之后,对于慢速指针所走的每 j 步,快指针将走 k 步,因此它将走 O(kj) 步。由于 j = O(n),净运行时间最多为 O(nk)。请注意,这表示我们使用快速指针执行的步骤越多,算法完成所需的时间就越长(尽管只是成比例地如此)。因此,选择 k = 2 可以最大限度地减少算法的整体运行时间。

希望这会有所帮助!

【讨论】:

你的证明不是假设你知道你试图找到的循环的长度,这样你就可以为兔子选择一个合适的速度。虽然这将产生一个始终在该周期长度内工作的野兔,但不能保证在不同长度的周期内工作(除非您选择速度 2)。 @fd- 证明本身并不假设你知道循环长度;它只是说对于任何循环长度和循环起始位置,都有一些位置 j 具有所需的属性。如果您考虑修改后的 tortise/hare 算法将如何工作,它将开始以速率 1 和 k 推进两个指针。走 j 步后,两个指针将位于重合的 j 和 jk 位置。你不需要知道 j 是什么来达到它。这有意义吗? @Nikita Rybak- 是的。该算法的运行时间与步长成正比,这就是我们通常选择 2 的原因。 致那些投反对票的人——你能解释一下这个答案有什么问题吗? 美丽的解释。在盯着“让 j 是 l 大于 s 的最小倍数”一分钟后,它点击了:这意味着如果你从一开始采取 j 步,你就在循环内(因为 j > s),如果你从那里再走 j 步,你会回到同一个地方(因为 j 是 l 的倍数)。因此,对于任何 j 步的倍数,同样必须成立。虽然我们不知道 j 是先验的,但我们知道它必须存在,并且我们有效地问“这是 j 吗?”每次移动后,我们不能错过它。【参考方案9】:

考虑一个大小为 L 的循环,这意味着第 k 个元素是循环所在的位置:xk -> xk+1 -> ... -> xk+L-1 -> xk。假设一个指针以 r1=1 的速率运行,另一个以 r2 的速率运行。当第一个指针到达 xk 时,第二个指针已经在循环中的某个元素 xk+s 处,其中 0 k+(m mod L) 处,第二个指针位于 xk+((m*r2+s) mod L)。因此两个指针碰撞的条件可以表述为满足同余的m的存在

m = m*r2 + s (mod L)

这可以通过以下步骤来简化

m(1-r2) = s (mod L)

m(L+1-r2) = s (mod L)

这是线性同余的形式。如果 s 能被 gcd(L+1-r2,L) 整除,则它有一个解 m。如果 gcd(L+1-r2,L)=1,肯定会出现这种情况。如果 r2=2 则 gcd(L+1-r2,L)=gcd(L-1,L)=1 并且总是存在解 m。

因此 r2=2 具有良好的性质,即对于任何循环大小 L,它都满足 gcd(L+1-r2,L)=1,因此保证即使两个指针从不同的位置开始,指针最终也会发生冲突。 r2 的其他值没有这个属性。

【讨论】:

非常有趣的是,双速野兔有这个额外的“start-anywhere”属性。我需要更好地理解模算术(除了“如果 s 可被 gcd(L+1-r2,L) 整除,它有一个解 m”除外)

以上是关于为啥在链表中查找循环时将指针增加 2,为啥不增加 3、4、5?的主要内容,如果未能解决你的问题,请参考以下文章

使用 Hare and Tortoise 方法在链表中检测循环

c_cpp 在链表中查找循环的长度

[LeetCode]138复制带随机指针的链表

Xamarin/C# - 在日期范围内查找周末的方法。为啥我陷入循环并且无法增加当前日期?

为啥要用Hash

在链表中添加节点时使用双指针的原因是啥?