为什么用快慢指针检测链表是否有环的时候,快指针的步长选择的是2,而不是3,4,5?

上个月在eBay上海进行了二面,一共4轮,每轮45分钟左右。感觉基础的数据结构,算法,Java语言的题目回答得还行,对于一些开放的Problem Solving类型的问题有点儿束手无策。自然,最后的结果也是被鄙视了。有些失落,不过最让我失落的不是没有回答好那些开放的题,而是被问到如标题所述的问题时,脑子一片空白。

在准备面试的过程中,就会发现,利用快慢指针检测单向链表中是否存在环的答案遍地都是。而这些答案中几乎一致地默认了设置快指针的步长为2。然后我们就记住了这些答案,信心满满地准备迎接面试。当然,大部分公司也就看你知不知道怎么检测出环,再深入一点儿就问你怎么确定环的起始位置。但是再深入一点儿呢?为什么快指针的步长非得是2,而不能是3,4,5,或者其他的呢?

这篇博客就是解释为什么步长选择的是2,同时提醒我自己知其然,而不知其所以然的代价。

问题:

  1. 给你一个单向链表,实现一个算法,判断这个链表中是否存在环;
  2. 如果存在环,返回环的起始结点;
  3. (如果这个时候你给出的实现是用的快慢指针,而且步长是2)步长选择3,4,5,或者其它的可以解决以上两个问题吗?如果也可以,那么为什么你选择的步长是2?


证明快慢指针在环中是可以相遇的:

证明这个问题?可能你会说,只要快慢两个指针都进入环中,两者有速度差,快指针就一定能够追上慢指针。可是这儿“追上”有可能是两种情况(注意,这儿讨论的是一般情况,快指针步长k >= 2),一是恰好追上(也就是相遇了),二是追上并超过了慢指针。当然,这两种情况都不妨碍我们判断是否有环存在。但是对于问题2,网上给出的解法通常都以恰好相遇为前提,求出起始结点的(至于怎么求,稍后会讲到)。那既然这样,我们就有必要证明无论快指针步长k为多少,快慢指针在环中都能恰好相遇。

证明开始:

首先,换个角度想问题。用一个步长为1的指针模拟遍历这个单向链表的过程。如上图所示,设单向链表的起始结点为X0(0是下标,可惜CSDN还不支持上下标输入),环的起始结点为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呢?

有了上面证明的基础,就不难发现 k 越小,所需的遍历次数就越少,因为给定一个带环的单向链表,j 的取值是确定的。所以,取 k = 2。

正规一点儿的分析,如下:

因为 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 ,可以最小化算法的运行时间。

如何确定环的起始结点?

在确定了 k 取2之后,我们考虑
  1. 当慢指针刚好进入环中,也就是慢指针走了 s 步之后,快指针走了 2*s 步,所以快指针在环中走了 2*s - s = s 步;
  2. 由于存在 s > cl 的情况,我们记快指针超出 Xs 的距离是 s % cl ;
  3. 此时,快指针需要追及慢指针的距离是 cl - s % cl;
  4. 因此,当慢指针在环中走了cl - s % cl 步后,快指针追上了慢指针;
所以,相遇之后的慢指针距离 Xs 的距离是 cl - (cl - s % cl) = s % cl 。因为有环的存在,我们可以把这个距离看成 s + M * cl,M 是正整数。所以相遇时的慢指针距离环的起始结点Xs 是 s 。这时,我们再设置另一个指针从单向链表的头开始,以步长为 1 移动,移动 s 步后相遇,而这个相遇结点正好就是环的起始结点。

总结

至此,证明完毕,同时也回答了题目中的3个问题。虽然有点儿偏数学的证明推倒,而且我觉得面试的时候没必要问这么细,但是万一面试官正好就问了这个问题,而你又能这样详细地说明解题思路,自然会给面试官留下一个非常好的印象,那么你拿到offer的概率也就更大。

References

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值