问题

142. 环形链表 II

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

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

不允许修改 链表。

示例 1:

输入:

head = [3,2,0,-4], pos = 1  

输出:

返回索引为 1 的链表节点  

解释: 链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:

head = [1,2], pos = 0  

输出:

返回索引为 0 的链表节点  

解释: 链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入:

head = [1], pos = -1  

输出:

返回 null  

解释: 链表中没有环。

提示:

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

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

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* detectCycle(ListNode* head) {
    
    }
};

分析

哈希存址法

想要知道链表是否有环,则需要确定判断条件,判断是否有环的条件,从环的拓扑上来说,就是一个数量在1以上的链表的next总是指向下一个节点,然后这下一个节点是之前的其中一个节点,从遍历的特征上来看,单链表会陷入循环中。

也就是说,如果每次遍历的时候,都保存之前的节点地址,只要在节点大于1的时候,下一个节点地址是之前保存的节点地址,那么,节点就存在环,首次发现节点地址重复出现的地方即为环的入口,为了缩减查找耗时,可以使用HASH表保存之前的地址。
但是,这样子的话,随着节点数量K的增加,占用的内存会变成O(K),题目要求需要使用 O(1) 的空间复杂度来解决问题,因此有没有办法让之前节点地址不进行保存,或者只保存最少的固定的部分以实现O(1)的空间复杂度呢?

快慢指针法

如果之前的节点地址不进行保存的话,那么是无法判断链表是否有环的(因为单链表地址无法和任何东西进行比较),无法比较就无法判断,无法判断就没办法消除是否有环的不确定性。

因此为了进行比较,至少要保存之前的(部分)地址。

为了解决此问题,使用两个速度不一样的指针。

快慢指针法就像是两个不同速度的运动员在环行跑道跑步,把问题转化成为了追及问题。

起点相同查找环

当节点A每次前进1步,如果有环,设定环入口为,环的长度为,那么节点A总是能回到环入口
无论加入多少不同速度的节点进行步进,从环入口出发,新增的节点总是能回到环入口
在节点A总是不停步进的情况下,如果再引入一个速度“更快”但是速度不变的节点B,这个更快的节点相对于原节点的速度设定为k。

那么如果他们同一点出发,当节点A走完1圈的时候,节点B也会走完k圈,这样的话,他们就会在入口相遇。
这时候可以通过二者的地址判断是否相同,相同的话,就认为链表有环。

起点不同查找环

现在考虑如果是从不同点同时出发,假设二者之间的距离为,B总是比A多走1步,如果A走完1圈的时候,节点B也会走完2圈,但是并不会相遇,想要相遇,就必须让节点B,多走的距离。

假设节点B移动后的位置是,节点A移动后的位置是,让A点作为环的起点,B点领先A点距离,设定m为整数,为环的长度。

现在如果能搞清楚的关系,就能知道相遇条件。
设A的速度为 ,每次只走1步,那么,设定A走过的步数为t,那么A新位置为:
设B的速度为,那么B走过的距离为:
根据已知条件,B总是比A快u倍,因此:
考虑到B位置是在处,因此有B的新位置为:

B比A快1步的时候,u=2,即B新位置为:

因为移动的步数等于环长的时候,会回到原来位置,将A相对于起点的位置设为,B相对于起点位置设置为,易知在起始位置的时候,那么有:

即:

其中是整数,​ 和 ​ 分别表示走过整圈的次数,它们的选取使得 ​ 均落在区间 内。

当B比A多走距离的时候,他们就会相遇,可以知道相遇条件为:

综合上式可得:

即:

其中是整数。 这个公式的含义是,当走过的步数和AB距离的和等于环的长度的倍数时候,也就是B多走的步数为 时候,AB就会相遇。

现在用数学归纳法进行证明。

基步证明:证明 的时候,条件成立。

,这时
这时有:

即:

故有:

即经过步的时候,A和B相遇在 处相遇,即基部成立。

代个容易理解的数值的话,表示如下:
可取, 的时候,那么在t=3步的时候,AB就能相遇。
这时候有:

也就是这时候相遇的位置是 3 。

假设 的时候, 也成立,其中k正整数。

的时候,有:

这时有:

即:

当 n = k 成立的时候,证明 n = k + 1 也成立

当n = k+1的时候,有:

这时有:

则:

即当 n = k+1 的时候,经过 步,这时A和B相遇,相遇点在

也就是说,一个速度为1的指针A和另一个速度为2的指针B,在相距的距离同时出发,如果链表有环,那么二者必然相遇,相遇的位置在处,其中为环长。

快指针速度的推广

当B比A快u倍,而u不为2的时候,容易推广出u为任意倍数的情况:

那么有公式:

得到:

当A和B相遇时有:

这时候可得:

即:

其中是整数。 这个公式的含义是,经过 步时,A和B相遇,相遇点在。 需要注意的是,当 的时候,没有意义,公式和步数无关,也就是说,环内两个不同出发点,但是速度相同的指针永远不会相遇。

寻找环入口

当u=2, A和B同时前进,当A到达环入口的时候,B刚好走过2倍A的距离,设起点为C点,设环入口为0点,那么A到达入口的时候,有入口位置 ,A经过的距离设定为,B经过的距离设定为,因为B是A的2倍速度,
因此有:

因为A只走过距离,换而言之,B在进入环内后,又多走了的距离,也就是说,B在环内的位置。
当A刚好进入环入口的时候,设定B相对于A的位置为,令环长 ,容易知道:

其中是整数,用于保障B的终点在区间 内。
在这时候,A在环的入口,让A和B同时继续移动,因为参考系相同的缘故,这时候可以套用起点不同查找环中的方法,可以知道,当A和B相遇的时候,A和B是在节点内的位置。

在这时,在起点C位置新增一个每次步进1位的节点C,节点C和节点A速度一样,节点C和节点A同时出发,当节点C到达环入口的时候,移动了距离。这时候节点C的位置是在0点,即
这时,节点A同样也移动了的距离,节点A累计移动的距离:

其中 用于保证节点A的位置在区间 内。
即:

这时节点A刚好也到达了环的入口。也就是

也就是说,一个速度为1的指针A和另一个速度为2的指针B在同一个初始位置同时出发,当A和B相遇的时候,在初始位置再增加1个速度为1的指针C,再让指针A和C同时前进,指针A和C就会在环的入口相遇。

代码

这里只给出快慢指针法的代码。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* detectCycle(ListNode* head) {
	    ListNode* first = head;
	    ListNode* second = head;
	    bool hasCycle = false;
        while (second && second->next) {
            first = first->next;
            second = second->next->next;
            if (first == second) {
                hasCycle = true;
                break;
            }
        }
        if (hasCycle) {
            ListNode* three = head;
            while (first != three) {
                three = three->next;
                first = first->next;
            }
            return three;
        }
        return nullptr;
	}
};

*****