一、链表基础知识
链表是一种线性数据结构,由若干个节点组成。每个节点包含两部分信息:一是节点存储的数据,二是指向链表中下一个节点的指针。在单链表中,每个节点的指针仅指向其后继节点,形成一个链式结构。
与数组相比,链表不需要连续的内存空间,节点可以分散在内存的不同位置,通过指针将它们连接起来。这种特性使得链表在插入和删除节点时更加高效,但也带来了一些特殊的问题,环形链表就是其中之一。
当链表中某个节点的指针不是指向 null,而是指向了链表中之前的某个节点时,就形成了环。在这种情况下,遍历链表的操作会陷入无限循环,因为永远无法到达链表的尾部。
二、判断链表中是否有环(LeetCode 141)
判断链表中是否存在环,是处理环形链表问题的基础。下面介绍两种常用的解法。
(一)哈希表法
哈希表法的思路简单直接:遍历链表,将每个访问过的节点存储在哈希表中。如果在遍历过程中,遇到一个节点已经存在于哈希表中,说明该节点被访问过两次,即链表中存在环;如果遍历结束后,所有节点都只被访问过一次,且最后一个节点的指针指向 null,则链表中不存在环。
具体实现代码如下:
public class Solution {
public boolean hasCycle(ListNode head) {
Set<ListNode> seen = new HashSet<>();
while (head != null) {
if (seen.contains(head)) {
return true;
}
seen.add(head);
head = head.next;
}
return false;
}
}
哈希表法的时间复杂度为 O (n),其中 n 是链表的长度,因为每个节点最多被访问一次。空间复杂度为 O (n),因为在最坏情况下,需要存储链表中所有的节点。
(二)快慢指针法
快慢指针法是一种更优的解法,它不需要额外的存储空间。其核心思想是设置两个指针,慢指针每次向前移动一步,快指针每次向前移动两步。
如果链表中不存在环,快指针会先到达链表的尾部,此时可以判断链表中没有环。如果链表中存在环,快指针会在环中不断循环,而慢指针进入环后,由于快指针的速度比慢指针快,最终快指针会追上慢指针,即两个指针会相遇,此时可以判断链表中存在环。
具体实现代码如下:
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while (slow != fast) {
if (fast == null || fast.next == null) {
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
}
快慢指针法的时间复杂度为 O (n),其中 n 是链表的长度。在最坏情况下,当链表存在环时,快指针需要绕环多圈才能追上慢指针,但总体的时间复杂度仍然是线性的。空间复杂度为 O (1),只需要常数级别的额外空间。
三、寻找环形链表的入口节点(LeetCode 142)
在判断出链表存在环之后,接下来的问题是找到环的入口节点。同样,我们可以采用哈希表法和基于快慢指针的方法来解决。
(一)哈希表法
与判断链表是否有环的哈希表法类似,我们遍历链表,将每个访问过的节点存储在哈希表中。当第一次遇到一个已经存在于哈希表中的节点时,这个节点就是环的入口节点。
具体实现代码如下:
public class Solution {
public ListNode detectCycle(ListNode head) {
Set<ListNode> seen = new HashSet<>();
while (head != null) {
if (seen.contains(head)) {
return head;
}
seen.add(head);
head = head.next;
}
return null;
}
}
这种方法的时间复杂度和空间复杂度与判断环是否存在的哈希表法相同,分别为 O (n) 和 O (n)。
(二)快慢指针法
基于快慢指针法寻找环的入口节点,需要利用快慢指针相遇时的一些特性。
首先,设置慢指针和快指针,慢指针每次走一步,快指针每次走两步。当两个指针相遇时,说明链表中存在环。此时,将快指针重新指向链表的头节点,然后让慢指针和快指针以相同的速度(每次走一步)向前移动,当它们再次相遇时,相遇的节点就是环的入口节点。
为什么这样可以找到环的入口节点呢?我们来进行简单的推导。
设链表的头节点到环的入口节点的距离为 a,环的入口节点到快慢指针相遇节点的距离为 b,相遇节点到环的入口节点的距离为 c。则环的长度为 b + c。
当快慢指针相遇时,慢指针走过的距离为 a + b,快指针走过的距离为 a + b + k*(b + c),其中 k 是快指针在环中绕的圈数。由于快指针的速度是慢指针的两倍,所以快指针走过的距离也是慢指针的两倍,即:
2∗(a+b)=a+b+k∗(b+c) 2*(a + b) = a + b + k*(b + c) 2∗(a+b)=a+b+k∗(b+c)
化简可得:
a+b=k∗(b+c),
a + b = k*(b + c),
a+b=k∗(b+c),
即:
a=k∗(b+c)−b=(k−1)∗(b+c)+c
a = k*(b + c) - b = (k - 1)*(b + c) + c
a=k∗(b+c)−b=(k−1)∗(b+c)+c
这表明,从链表的头节点到环的入口节点的距离 a,等于从相遇节点绕环 (k-1) 圈后再到环的入口节点的距离。因此,当快指针从表头出发,慢指针从相遇节点出发,两者以相同速度移动时,它们会在环的入口节点相遇。
具体实现代码如下:
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null) {
return null;
}
ListNode slow = head;
ListNode fast = head;
boolean hasCycle = false;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
hasCycle = true;
break;
}
}
if (!hasCycle) {
return null;
}
fast = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
这种方法的时间复杂度为 O (n),空间复杂度为 O (1),是一种高效的解法。