详解为什么用一步两步快慢指针?三步四步可以吗

解题思路
拿到这道题,不知道你的第一反应会是什么,知道套路的聪明小伙伴可能马上就说:这还不简单,直接快慢指针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;
    }
}
posted @ 2021-02-22 20:37  预言2018  阅读(1380)  评论(0)    收藏  举报