在之前的题目中,我们接触到的都是无环链表,对于环形链表而言,它任何一个节点的next指针都不指向null,一定会有一个指针的next指向链表中的另一个节点。看题:
142.环形链表 II
首先,如何判断链表是否有环?常用的方法是快慢指针同时出发,快指针1次走2步,慢指针一次走一步,如果链表有环,快慢指针一定会相遇。这点很好理解,想象一下小学的追及相遇问题,快慢指针入环后,一直在绕圈跑,速度不一致,肯定会有相遇的一刻。
那么,环的入口怎么找呢?
如下图,设入环之前的路程为x,入口点为entry,相遇点为O。入口点到相遇点的距离为y,相遇点返回到入口点的距离为z。
快慢指针相遇时,它们耗费的时间是一致的,速度关系我们是知道的,那么就可以通过这个来分析路程关系。
S s l o w = x + y S f a s t = x + y + n ( y + z ) \begin{align} &S_{slow} = x + y \\ &S_{fast} = x + y + n(y+z) \end{align} Sslow=x+ySfast=x+y+n(y+z)
快指针要遇到slow,肯定要多跑 n ( n > = 1 ) n(n>=1) n(n>=1)圈。
slow指针一定在一圈之内就被fast追上吗?
是的。不妨这么假设:
小快每秒跑200m,小慢每秒跑100米。两人约定一起绕田径场跑。田径场周长400m。
小快先开始跑,若干秒后小慢也开始跑。以此时为0时刻,画一下他们俩的相对位置:
2s时刻,小快已经跑完了一圈,小慢也跑到了中点;
此时,小慢跑到终点还需2s,但是小快跑到终点的时间一定不超过2s。也就是说,最慢在终点之前,小快一定能追上小慢。
也就是说,slow指针一定会在1圈内和fast相遇。
再来看这张图:
由运动公式
t
=
s
/
v
t = s/v
t=s/v,计算出x的表达式如下:
x
+
y
1
=
x
+
y
+
n
(
y
+
z
)
2
x
+
y
=
n
(
y
+
z
)
x
=
(
n
−
1
)
(
y
+
z
)
+
z
\begin{align} \frac{x+y}{1} &= \frac{x+y+n(y+z)}{2}\\ x+y &= n(y+z)\\ x &= (n-1)(y+z) + z \end{align}
1x+yx+yx=2x+y+n(y+z)=n(y+z)=(n−1)(y+z)+z
可以发现一个非常关键的点:
x的长度等于n-1圈的周长加上z的长度。
这说明什么?如果快慢指针分别同时从O点、链表起点出发,快指针先走长度z到达entry之后,再走n-1圈回到entry,而此时慢指针才刚好走到entry。
也就是说,二者以相同的速度,都走x步,一定会在entry处相遇!
也就是说,两个指针分别从O点和起点出发,一定会在环入口处相遇!
所以,n的值不重要,无论快指针兜兜转转多少圈,无论慢指针离环的入口有多远,它们一定会相遇。
诶,是不是有点像算法第一深情160.相交链表了?哈哈。
捋一捋我们上面说的两个关键点:
- 声明快慢指针,快指针速度是慢指针的两倍,二者一定会在交点或null相遇;
- 声明两个指针分别从起点和相遇点出发,一定会在入口处相遇。
想通之后,编码就非常简单了。
由于快慢指针会在一开始时相遇,就用了do-while。
if(head==null) return null;
ListNode fast = head,slow = head;
do{
slow = slow.next;
if(fast!=null){
if(fast.next!=null){
fast = fast.next.next;
}else{
return null;
}
}
}while (slow!=null && slow != fast);
//二者在null处相遇,说明没有环
if(fast==null) return null;
//fast不动,slow回到起点,一起前进
slow = head;
while (slow != fast){
slow = slow.next;
fast = fast.next;
}
return fast;