「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」
题目
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 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) 空间解决此题?
解题
解题一:判断重复节点(HashSet)
思路
从头节点开始,依次遍历单链表中的每一个节点,用 HashSet 存放曾经遍历过的 Node 节点,每遍历一个节点判断 HashSet 是否有相同的值,如果发现 HashSet 中存在与之相同的的节点,则说明链表有环,返回 True,如果发现 HashSet 中不存在与之相同的的节点,就把这个新节点存入 HashSet 中,之后进入下一个节点,继续重复刚才的操作。遍历完成后还没有返回值,就返回 -1
代码
/**
* Definition for singly-linked list.
*/
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
public class Solution {
public ListNode detectCycle(ListNode head) {
// 当链表为空,或者只有一个节点时,不可能构成环形链表,返回 head
if (head == null || head.next == null) {
return null;
}
// 存放已经遍历过的节点,用于判断是否有相同节点
HashSet<ListNode> resultSet = new HashSet<>();
// 遍历链表
while (head != null) {
if (resultSet.contains(head)) {
// 当 resultSet 存在相同节点时,代表形成环形链表,返回 true
return head;
}
// 当resultSet 不存在相同节点时,将遍历的节点存入 resultSet,继续遍历链表
resultSet.add(head);
head = head.next;
}
// 链表已经遍历完,没有相同节点,代表没有构成环形链表
return null;
}
}
总结
特点
- 使用了 HashSet 作为额外的缓存
注意点
- 判断是否有相同的节点使用 Node 判断,而不是 Node.val 判断
- 使用 HashSet 比 HashMap 能更节省空间
性能分析
- 时间复杂度:O(n)
- 空间复杂度:O(n)
- 执行用时:3 ms,在所有 Java 提交中击败了 21.92% 的用户
- 内存消耗:38.9 MB,在所有 Java 提交中击败了 19.26% 的用户
解题二:双指针法(快慢指针)
思路
将链表有环问题改变为追及问题
- 判断链表是存在环
- 首先创建两个指针 p1 和 p2,让它们同时指向这个链表的头节点。然后开始一个大循环,在循环体中,让指针 p1 每次向后移动 1 个节点,让指针 p2 每次向后移动 2 个节点,然后比较两个指针指向的节点是否相同。如果相同,则可以判断出链表有环,如果不同,则继续下一次循环,直到 p2 指针指向空
- 判断链表环的长度
- 当两个指针首次相遇,证明链表有环的时候,让两个指针从相遇点继续循环前进,并统计前进的循环次数,直到两个指针第 2 次相遇,此时,统计出来的前进次数就是环长,因为指针 p1 每次走 1 步,指针 p2 每次走 2 步,两者的速度差是 1 步。当两个指针再次相遇时,p2 比 p1 多走了整整 1 圈。因此,环长 = 每一速度差 * 前进次数 = 前进次数
D:代表从头节点到首次相遇节点的距离S1:代表从入环点到首次相遇点的距离S2:代表从首次相遇点到入环点的距离S1+S2:代表环的长度
- 计算从头节点到入环节点的长度
- 当两个指针首次相遇时,指针 p1 一次只走 1 步,所走的距离时
D+S1,指针 p2 一次走 2 步,多走了 n 整圈,所走的距离是D+S1+n(S1+S2) - 由于 p2 的速度是 p1 的 2 倍,所以所走的距离也是 p1 的 2 倍,因此
2(D+S1) = D+S1+n(S1+S2)->D=(n-1)(S1+S2)+S2,也就是说,从链表头节点到入环的距离等于从首次相遇点绕环 n-1 圈再回到入环点的距离 - 把其中一个指针放回到头节点位置,另一个指针保持在首次相遇点,两个指针都是每次向前走 1 步,那么他们最终相遇的节点就是入环点
- 当两个指针首次相遇时,指针 p1 一次只走 1 步,所走的距离时
代码
/**
* Definition for singly-linked list.
*/
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
public class Solution {
public boolean hasCycle(ListNode head) {
// 当链表为空,或者只有一个节点时,不可能构成环形链表
if (head == null || head.next == null) {
return null;
}
// p1 指针指向头节点的下一个节点
ListNode p1 = head.next;
ListNode p2 = null;
// p1 指针指向头节点的下一个节点
if(p1.next !=null){
// 当 p1 的下一个节点不为 null 时,设置 p2 指针指向 p1 指针的下一个节点
p2 = p1.next;
}
while(p2 !=null && p2.next !=null && p2.next.next !=null){
// 满足 p1 走 1 步,p2 走 2 步,开始遍历链表
if (p1 == p2) {
// 当 p1 == p2 时,代表构成循环链表,将 p1 放回头节点,p1 和 p2 都只走一步,相遇点就是入环点
p1 = head;
while (p1 != p2) {
p1 = p1.next;
p2 = p2.next
}
return p1;
}else {
// 当 p1 != p2 时,继续循环链表,p1 走 1 步,p2 走两步
p1 = p1.next;
p2 = p2.next.next;
}
}
// 链表已经遍历完,没有相同节点,代表没有构成环形链表
return null;
}
}
动态图片
总结
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 执行用时:0 ms,在所有 Java 提交中击败了 100% 的用户
- 内存消耗:38.4 MB,在所有 Java 提交中击败了 73.72% 的用户