环形链表

205 阅读4分钟

一、环形链表

题目

给定一个链表,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回 true 。 否则,返回 false

示例:

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

思路

使用快慢指针:

  1. 先初始化快慢指针 fastslow 指向头节点
  2. 快指针每次走两步,慢指针每次走一步
  3. 若链表成环,最终快慢指针会指向同一个节点
  4. 若链表不成环,快指针会先走到链表的尾部

代码

function hasCycle(head) {
  // 初始化快慢指针
  let fast = slow =  head;
  while(fast !== null && fast.next !== null){// 链表可能有奇数个节点或偶数个节点
    // 慢指针每次走一步
    slow = slow.next;
    // 快指针每次走两步
    fast = fast.next.next;
    // 如果成环必相遇
    if(slow === fast) return true;
  }
  // 快指针走到链表尾时还没相遇,说明不成环
  return false;
};

二、环形链表II

题目

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中

说明:不允许修改给定的链表。

示例:

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

思路

由上文双指针方法可知环形链表的相遇点在哪,此时在相遇点处令快指针(或慢指针)指向环形链表的起始节点(头节点),然后令快、慢指针以相同步伐继续走,则快、慢指针再次相遇处即为环的起点。

为什么上述思路就是正确的呢?

👭友情提示:下面的证明会牵涉到一些数学运算,可能有点绕,但是认真阅读、仔细思考后应该都能看懂

证明如下证明如下 \Downarrow


快、慢指针从环形链表头节点开始,令快指针每秒走 22 步,慢指针每秒走 11 步,快、慢指针走到相遇处时花了 xx 秒。

那么在相遇处时快指针共走了 2x2x 步,慢指针共走了 xx 步。假设快指针走的环的圈数为 mm 圈,慢指针走的环的圈数为 nn 圈。那么此时,对于快指针就有如下等式

2x=mS+Y+L (1)2x=m*S+Y+L \text{ (1)}

对于慢指针就有如下等式

x=nS+Y+L (2)x=n*S+Y+L \text{ (2)}

(2)2(1)(2)*2-(1) 可得 0=(2nm)S+Y+L0=(2n-m)*S+Y+L ,即 L=(m2n)SYL=(m-2n)*S-Y。由于 L0L\geqslant 0 ,所以 m2nm-2n 的值应为 1\geqslant 1 的整数,那么可得 L=(m2n1)S+SYL=(m-2n-1)*S+S-Y。不妨令 p=m2n1p=m-2n-1,此时 L=pS+SYL=p*S+S-Y,其中 p0p\geqslant 0

由于在相遇时令快指针指向环形链表的头节点,然后快、慢指针以相同步伐继续走。当快指针走到环的起点时,也就走了 LL 步,又因为 L=pS+SYL=p*S+S-Y,所以快指针走了 pS+SYp*S+S-Y 步。由于快、慢指针是以相同步伐走的,也就是说慢指针从相遇处开始走也走了 pS+SYp*S+S-Y 步,所以此时我们只要证明慢指针从相遇处开始走了 pS+SYp*S+S-Y 步后恰好停在环的起点即可证明上述的思路是正确的。而从相遇点顺时针方向环的起点距离恰好为 SYS-Y 步,那么慢指针走的 pS+SYp*S+S-Y 步用文字描述的话就可以是:慢指针先从相遇处开始走了 SYS-Y 步到环的起点,再在环的起点沿顺时针方向又走了 pp 圈,也就是 pSp*S 步,最终还是回到环的起点与快指针相遇。


证明证明 \quad 毕 \Uparrow

代码

// 若一个链表成环,返回环的起始节点
function detectCycle(head) {
  // 初始化快、慢指针指向头节点
  let slow = head;
  let fast = head;
  while (fast !== null && fast.next !== null) {
    // 慢指针每次前进一步
    slow = slow.next;
    // 快指针每次前进两步
    fast = fast.next.next;
    // 如果存在环,快慢指针必然相遇,退出当前while循环
    if (slow === fast) break;
  }
  // 让快指针重新指向头节点
  fast = head;
  while(true){
    // 让快慢指针以相同步伐前进
    slow = slow.next;
    fast = fast.next;
    // 快慢指针相遇的点即为环的起始节点
    if(slow === fast) return slow;
  }
}

后记

本篇文章(环形链表)的难点之处有两:

  • 第一:如何想到利用快慢指针去解这种环形链表的题呢?
    • 多刷题呗
  • 第二:如何证明快慢指针的思路是正确的呢?
    • 需要一定的数学推理能力

该文章主要用于平时的复习巩固和积累,若有书写错误的地方还望各位大佬不吝赐教🙏。

参考资料