详解为什么用一步两步快慢指针?三步四步可以吗
解题思路
拿到这道题,不知道你的第一反应会是什么,知道套路的聪明小伙伴可能马上就说:这还不简单,直接快慢指针KO啊!
确实没错,我们刷题时或许可以飞快 AC 完事,但是在真实面试场景时万一碰到较真的面试官问你:
怎么证明快慢指针在环中是可以相遇的?
为什么快指针步长要为 2 ?3,4,5可以吗
如何确定环的起始结点呢?
当面临这样的问题时,不知道你的反应又会是怎样呢? 那么接下来我们一起来探讨这种猝不及防的开放性问题。
怎么证明快慢指针在环中是可以相遇的?
证明这个问题?可能你会说,只要快慢两个指针都进入环中,两者有速度差,快指针就一定能够追上慢指针。可是这儿“追上”有可能是两种情况(注意,这儿讨论的是一般情况,快指针步长k >= 2),一是恰好追上(也就是相遇了),二是追上并超过了慢指针。当然,这两种情况都不妨碍我们判断是否有环存在。但是对于问题2,网上给出的解法通常都以恰好相遇为前提,求出起始结点的(至于怎么求,稍后会讲到)。那既然这样,我们就有必要证明无论快指针步长k为多少,快慢指针在环中都能恰好相遇。
证明开始:

首先,换个角度想问题。用一个步长为1的指针模拟遍历这个单向链表的过程。如上图所示,设单向链表的起始结点为X0(0是下标),环的起始结点为Xs。自然,遍历到最后就是一遍遍的重复这个环。如果,我们把遍历的项都不断记录到一个数组中的话(下标按次序递增),最后就是一定长度的序列串的重复。例如,
X0, X1, ... , Xs, Xs+1, ..., Xs+cl-1
Xs+cl, Xs+cl+1, ..., Xs+2*cl-1
...
cl (circle length)表示环的长度。
假设 j 是 cl 的整数倍,并且是 cl 整数倍中满足 j > s 最小的那个数。对于任意的 k (k >= 2),我们考虑下标分别为 j 和 jk 的两个位置 Xj 和 Xjk。可见,Xj 就是慢指针走了 j 步之后的位置,而 j > s 则保证了此时慢指针已经进入环中。同理,此时的快指针在位置 Xjk,必然也在环中。
因为 j 是 cl 的整数倍,我们可以把 Xjk 看成是一个指针从位置 Xj 开始,走了 (k - 1) 次的 j 步。而每走 j 步,在单向链表的环中,其实又回到了 Xj 位置,因为 j 是 cl 的整数倍。所以我们有,Xj = Xjk 。这样,我们就证明了快慢指针肯定会恰好相遇的问题。
为什么快指针步长要为 2 ?3,4,5可以吗
有了上面证明的基础,就不难发现 k 越小,所需的遍历次数就越少,因为给定一个带环的单向链表,j 的取值是确定的。所以,取 k = 2。当然,除了 1 ,别的其他数都可以。
正规一点儿的分析,如下:
因为 j 是 cl 的整数倍,并且是 cl 整数倍中满足 j > s 最小的那个数。如果 s <= cl,那么 j = cl;如果 s > cl,那么 j < 2*s ,所以 j 的时间复杂度为 O(s + cl)。假设单向链表中结点的个数为 n,因为 s 和 cl 都是小于 n 的,所以 j = O(n) 。这是慢指针的时间复杂度,那么快指针的就是 O(nk) 。所以取 k = 2 ,可以最小化算法的运行时间。
如何确定环的起始结点呢?

如上图,紫色点为快慢指针首次相遇点,入环前距离为【a】,慢指针入环后走过的距离为【b】,环剩下距离为【c】
首次相遇
慢指针 S = a + b
快指针 F = a+ n(b + c) + b 其中n>=1,快指针起码走了一圈以上才可能相遇
又因为 F = 2S 慢指针走一步,快指针走两步
代入1,2 可得 a + n(b + c) + b = 2(a + b)
各种移项可得 a = (n-1)b + nc = (n-1)(b + c) + c
其中 n为快指针绕的圈数
n=1时, a = c
n=2时, a = 一圈 + c
n=3时, a = 两圈 + c
.....
所以其实我们并不关心绕了多少圈,就知道 n圈+c就是入环点了
人为制造碰撞机会,让快指针重新出发(但这次一次走一步),只要碰撞了,就是入环位置了,无需考虑慢指针在环里绕了多少圈
文字不够清晰的话,我特意画了图直观展示:









既然讲了入环结点,那我们顺其自然引出进阶题「环形链表 II」,就是要求返回链表开始入环的第一个节点。

理解了以上,相信你写出两题的代码都不在话下。
之所以费这么多笔墨讲解比较多的数学性证明推倒,是希望你不仅能知其然,更能知其所以然,虽然面试时不一定会问这么细,但是万一面试官恰巧就问了,而你又能详解地说明,自然会给面试官留下一个非常好的印象,那么你从众人中脱颖而出的概率将大大增加。
代码实现
环形链表
//go /** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */ func hasCycle(head *ListNode) bool { // 如果为空 或 一个节点,肯定无环 if head == nil || head.Next == nil { return false } slow := head // 慢指针 fast := head.Next // 快指针 // 不重合执行循环 for slow != fast { if fast == nil || fast.Next == nil { // 到链表尾部无环 return false } slow = slow.Next // 慢指针走一步 fast = fast.Next.Next // 快指针走两步 } // 重合返回true return true }
//java public boolean hasCycle(ListNode head) { if (head == null || head.next == null) { return false; } ListNode slow = head; ListNode fast = head.next; while (slow != fast) { if (fast == null || fast.next == null) { return false; } slow = slow.next; fast = fast.next.next; } return true; }
环形链表 II
//go func detectCycle(head *ListNode) *ListNode { fast, slow := head, head //获取首次相遇时候,slow的位置 for fast != nil && fast.Next != nil { fast = fast.Next.Next slow = slow.Next if slow == fast { break } } //如果快指针走到尽头,没环 if fast == nil || fast.Next == nil { return nil } //快指针重新出发,相遇位置就是入口位置 fast = head for fast != slow { fast = fast.Next slow = slow.Next } return slow }
//java public class Solution { public ListNode detectCycle(ListNode head) { ListNode fast = head, slow = head; //获取首次相遇时候,slow的位置 while(fast!= null && fast.next != null){ fast = fast.next.next; slow = slow.next; if(fast == slow){ break; } } //如果快指针走到尽头,没环 if(fast == null || fast.next == null) return null; //快指针重新出发,相遇位置就是入口位置 fast = head; while(fast != slow){ fast = fast.next; slow = slow.next; } return slow; } }

浙公网安备 33010602011771号