题目
🔗题目链接:142. 环形链表 II - 力扣(LeetCode)
给定一个链表的头节点 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 <= 105pos的值为-1或者链表中的一个有效索引
进阶: 你是否可以使用 O(1) 空间解决此题?
思路
先确认链表是否有环,再想办法找到链表入环的第一个节点。
-
确认链表是否有环。
使用快慢指针。快指针一次走两步,慢指针一次走一步,快慢指针相等(指向同一个节点)说明。快指针相对于慢指针每次移动一步,这样快指针在移动的过程中就不会跳过慢指针。
在这里要注意,快指针一定是先入环慢指针后入环,然后在环内快指针追上慢指针(入环之前,快指针一定是在慢指针之前,且快指针跑得比慢指针快。所以在入环之前,快慢指针绝不可能相遇)
-
找到链表入环的第一个节点
假设从头节点到环的入口节点的节点数是 x ,环入口节点到快慢指针相遇的节点数为 y ,从相遇节点再到环入口节点的节点数为 z 。如图所示:
fast 为快指针,slow为慢指针。
那么相遇时,slow 走过的节点数为
x + y,fast 走过的节点数为x + y + n (y + z), n 为 fast 在环内走了 n 圈才与 slow 相遇,y + z为一圈内节点的个数。假设 fast 和 slow 从开始到相遇的时间为 t,可以得到以下公式:
我们要求的是环的入口节点,所以我们要求出 x,那么公式就可以变为这样:
x = n (y + z) - y
x = ny + nz - y
x = (n - 1)y + nz
x = (n - 1)y + (n-1)z + z
x = (n - 1)(y + z) + zn 代表 fast 在环内转的圈数, n >= 1。
当 n = 1 时,fast 在环内转了一圈就和 slow 相遇了。此时 x = z。也就是在相遇处定义一个 index1,在头节点定义一个 index2,让他俩同时移动,且每次前进一个节点,那么他们(index1、index2)相遇的地方就是环入口节点。
当 n > 1 时,就是 fast 在环内转了 n 圈才遇到 slow 。这种情况其实和 n = 1 时的效果是一样的。只不过是 index1(在相遇处定义的指针) 在环内转 (n - 1) 圈,然后遇到的 index2(在头节点定义的)。注:为什么是 index1 在环内转了 (n - 1) 圈,可以带入 n = 2 的情况模拟下。
代码
/**
* Definition for singly-linked list.
* class ListNode {
* val: number
* next: ListNode | null
* constructor(val?: number, next?: ListNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
* }
*/
function detectCycle(head: ListNode | null): ListNode | null {
if (!head) return head;
// 定义快慢指针
let fast = head;
let slow = head;
while (fast && fast.next) {
// fast 快指针一次走两步
fast = fast.next.next;
// slow 慢指针一次走一步
slow = slow.next;
// 当 fast === slow,也就是快慢指针相遇了,说明链表有环
while (fast === slow) {
// 相遇处定义一个 index1
let index1 = fast;
// 链表头定义一个 index2
let index2 = head;
// index1 与 index2 不相等,那就让他俩一直走下去
while (index1 !== index2) {
index1 = index1.next;
index2 = index2.next;
}
return index1;
}
}
return null;
};
-
时间复杂度 O(n)
快慢指针相遇前,指针走的次数小于链表长度(注意是次数,虽然 fast 一次走两个节点,但 fast 和 slow 走的次数是一样的。因为 slow 一次走一个节点,所以 slow 走的次数就是 slow 走的长度,而 slow 走的距离小于链表长度)
快慢指针相遇后,两个 index 指针走的次数也小于链表长度。
相遇前后走的两次长度都小于 n,所以走的从长度小于 2n。
-
空间复杂度 O(1)
没有申请额外的链表空间,只是定义了几个变量。