题目:现在有一个单链表,请判断它是否带环,如果构成环,请返回环的入口点。
这是接着上篇文章的讨论,留了两个问题,如何判断一个单链表是否构成环,有的话,返回它的入口点。之后我们再研究在构成环的链表中如何判断两个链表是否相交,这样我们上篇文章就完整了。
上篇当中,我们提到了构成环的结构应该是类似于6的样子对吧。我们回忆一下,小学做过的数学题哈,两个小朋友在操场上跑步,一个快,一个慢,请问他们是否会相遇。大家都知道快的会追上慢的对不对?现在的问题稍微要复杂一点,因为这个操场上多了一个直线型的跑道,我们要找出来,直线型跑道与圆形跑道的交点。我们先看图,然后我给出一些定义,带大家做些简单的数学推导:
- L: 单链表的长度
- A-B: 头结点到入口点的长度为a,B节点是我们要求的目标节点
- B-C: 入口点到相遇点的长度为x
- B-C-B: 环的长度为r
- S: 出发到相遇是的长度
那我们能够有这样的推导:
- 每次走一步的指针,S = a+x
- 每次走两步的指针一共走了 2S,
- 我们能够肯定它从A-B-C, 但是环内走了多少圈,不得而知,假设在环内已经绕了N圈,之后等到了慢的同学,则有 2S = S + Nr , 所以 s = Nr,
- 我们就得到了 a + x = Nr, a + x = (N-1) r + r
- 我们知道 L = a + x + (r-x), r-x 也就是 C-B,而 L = a + r, 我们得到 a + x = (N-1)r + (L-a),
- 所以 a = (N-1)r + (L-a-x)
实际意义: 上面的这个式子很关键,我们的目标是求出a,这意味着,我们如果一步一步的走,从A出发到入口点需要走的步骤,正好等于从相遇点到B点的步数。(L-a-x)其实就是图中紫色的大板块饼。也就是说,在相遇的位置,我们重新设一个指针,然后两个指针一起走,相遇的点就是入口点。
问题变得简单了,我们来看代码,至于怎么判断有环的,大家可以从里面体会出来,也可以关注我后序的视频课程、博客和公众号推送。
/// <summary>
/// 判断单链表是否有环,如果有的话我们返回入口点,否则为null
/// </summary>
/// <param name="linkNode">单链表</param>
/// <returns></returns>
public static MyLinkNode<T> FindEntryInCycle(MyLinkNode<T> linkNode)
{
var fast = linkNode;
var slow = linkNode;
MyLinkNode<T> meetPoint = null;
do
{
// 如果Slow 的 next有值,每次让 Slow 走一步
if (slow.Next != null)
{
slow = slow.Next;
}
// 如果 fast 的 next 有值,而且 fast 的 next 的 next还有值,让fast走两步
if (fast.Next != null && fast.Next.Next != null)
{
fast = fast.Next.Next;
}
// 走完一次,我们判断下,相遇了没有
if (fast == slow) meetPoint = fast;
}
// 如果没有相遇, 也没有到链表末尾,那我们继续,否则退出 do-while
// 大家想想为什么用 do-while, 而不是 while 哈
while (fast != slow && fast != null && slow != null);
// 不为空,说明有环,我们继续找相遇点, 否则不是环
if (meetPoint != null)
{
var newPoint = linkNode;
while (newPoint != slow)
{
newPoint = newPoint.Next;
slow = slow.Next;
}
return newPoint;
}
return null;
}
在这个基础上,再考虑两个可能有环的链表相交问题就变得简单了。下篇再续。好了,欢迎大家关注我的公众号,还有我的系列视频教程, 数据结构与算法 和 微软经典算法面试题辅导。大家有什么更好的解法,也欢迎讨论哈。