环型链表

原题链接https://leetcode-cn.com/problems/linked-list-cycle-ii/

一、问题描述

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

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

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

进阶:

你是否可以使用 O(1) 空间解决此题?
 

示例 1:

     408算法练习——环型链表_结点

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


示例 2:

     408算法练习——环型链表_408算法练习_02

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


示例 3:

     408算法练习——环型链表_每日一题_03

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。
 

提示:

链表中节点的数目范围在范围 [0, 104] 内
-105 <= Node.val <= 105
pos 的值为 -1 或者链表中的一个有效索引

 

二、问题分析

  如果不限制实际复杂度只需要维护一个标记键值对用来记录遍历到的每个结点,当再次遍历到已被遍历到的结点时说明找到了环。

  下面分析时间复杂度O(1)的情况。

    如果链表无环,则向后遍历必定能遍历到null结点,此时即可返回结果

    如果链表存在环,那么永远无法遍历到null结点,循环也就失效,此时使用快慢指针,维护快指针fast和慢指针slow,一起对链表进行遍历,快指针每次前进两步,慢指针每次前进一步。如果链表存在环,那么两个指针最后一定可以相遇,就像跑圈问题一样,一旦两个指针进入环路,而且速度不同,那么相遇是迟早的问题。那么就需要对环的环首结点进行判断,此时需要有数学技巧。

    分析环首结点(f与s相遇)

    快指针每次移动两个位置,慢指针每次移动一个位置,所以两个指针走过的路程之比为2:1,则有f=2s;

    两个指针在没进入环时,走过的路程都是相同的,因为进入环后就再也不会遍历到环外结点,那么设环外结点数为a,此外该事实还说明了两个结点走过路程的差异出现在环上,假设环内结点数为b,如果f指针绕环n周,则有f=a+nb;如果s指针绕环m周,则有s=a+mb;将f与s相减有f-s=(n-m)b;简化该公式,有f=s+xb;(这里n和m不一定是整数,但n-m一定会消去小数,也就是说x一定为整数。这很重要

    简单证明x为整数:假如s走过1/2圈后与f相遇,那么f一定走了整数圈+1/2圈。可以尝试反证法严格证明,并且将整数圈与不满一圈进行拆分。

    联立两个公式,得出f=2xb;s=xb;

    假设一个指针从头结点开始走,在走过a步后进入环首结点,然后再走b步又会回到环首结点,所以一个结点只要走a+nb步必定能走到环首结点,此时n为整数,

    综上,当f与s相遇后,s只需要再走a步就到达环首结点,因为a是环外结点数,那么从头节点引一个指针和s同时开始走,每次走相同的距离,二者相遇时必定是环首结点。

三、代码

  

 1 public class Solution {
 2     public ListNode detectCycle(ListNode head) {
 3         ListNode fast = head, slow = head;
 4         while (true) {
 5             if (fast == null || fast.next == null) return null;
 6             fast = fast.next.next;
 7             slow = slow.next;
 8             if (fast == slow) break;
 9         }
10         fast = head;
11         while (slow != fast) {
12             slow = slow.next;
13             fast = fast.next;
14         }
15         return fast;
16     }
17 }