题目
给定一个链表的头节点 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, 10^4]
内 -10^5 <= Node.val <= 10^5
pos
的值为-1
或者链表中的一个有效索引
进阶: 你是否可以使用 O(1)
空间解决此题?
题目分析
题目给出了一个链表的头节点head
,要求找到链表开始入环的第一个节点。如果链表中不存在环,则返回null
。
题目中还提到了一个整数pos
,该整数表示链表尾部连接到链表中的位置的索引(索引从0开始)。但是需要注意的是,pos
不作为参数传递,仅仅是为了标识链表的实际情况。
进一步理解题意,可以得到以下几个关键信息:
- 如果链表中存在环,那么环的入口节点之前的所有节点都不在环内。
- 链表中的节点数量范围在[0, 10^4]内。
pos
表示环的入口节点在链表中的索引,取值范围是[-1, 10^4]。
解题思路
这道题的解题思路可以借鉴快慢指针的思想。
我们可以使用两个指针,一个慢指针(slow)每次前进一步,一个快指针(fast)每次前进两步。通过判断两个指针是否相遇,可以确定链表中是否存在环。
在快慢指针相遇之后,将快指针重新指向链表的头节点,从链表的头节点开始和慢指针同步幅移动,直到它们再次相遇。相遇的位置就是环的入口节点。
为什么这样能找到环的入口呢?我们来分析一下:
假设链表的头节点到环的入口节点的距离为a,环的入口节点到快慢指针相遇的节点的距离为b,环的长度为c。
当慢指针走过距离s = a+b时,快指针已经走过了2(a+b)的距离。而快指针走过的距离等于慢指针走过的距离加上多次环的长度,即2(a+b) = a+b+nc,整理得到a + b = nc , 要想找到环的入口也就是找到a的长度,我们需要慢指针在环里转圈转到开始的位置,也就是慢指针走的路程是让s等于a+mc,现在s = a + b = nc,所以再让慢指针走一个a就可以了,但是我们又不知道a具体是多少,是不是再用一个指针开始从头和它一块移动就好了?两者还会在a处相遇。
根据上述分析,我们可以发现,当快慢指针相遇时,如果我们再让一个指针从头节点开始与慢指针同步移动,它们最终会在环的入口节点相遇。因此,该相遇位置即为环的入口节点。
代码实现
基于以上分析,我们可以实现以下代码:
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head or not head.next:
return None
slow = head
fast = head
# 判断是否存在环,快慢指针相遇
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
break
# 如果快指针为空,说明链表中不存在环
if not fast or not fast.next:
return None
# 将快指针重新指向头节点
fast = head
# 快慢指针继续移动,直到相遇
while slow != fast:
slow = slow.next
fast = fast.next
return fast
这段代码首先检查特殊情况:链表为空或只有一个节点,则肯定不存在环,直接返回None
。
然后,我们定义了两个指针slow
和fast
,并且初始化都指向链表的头节点。
接下来,使用快慢指针判断链表是否存在环。快指针每次走两步,慢指针每次走一步,如果它们相遇了,说明链表中存在环,否则不存在。由于这个过程可能会遍历整个链表,所以时间复杂度是O(N),其中N是链表中节点的数量。
如果链表中不存在环,那么快指针最终会指向链表尾部的None
,此时直接返回None
。
如果链表中存在环,那么快慢指针相遇的位置即为环内的某个节点。接下来,我们将快指针重新指向链表的头节点,然后快慢指针同时每次前进一步,直到它们再次相遇。相遇的节点就是环的入口节点。
最后,返回链表的入口节点。
复杂度分析
- 时间复杂度:O(N),其中N是链表中节点的数量。在最坏情况下,我们需要遍历整个链表才能确定是否存在环,以及找到环的入口节点。
- 空间复杂度:O(1),只使用了常数级别的额外空间。
扩展
计算环的长度
快慢指针
在快慢指针相遇后,可以继续移动慢指针,并且记录移动的步数,直到再次回到相遇点。记录的步数即为环的长度。
def detectCycleLength(head):
slow = head
fast = head
has_cycle = False
# 判断是否存在环,并找到相遇点
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
has_cycle = True
break
# 如果不存在环,则返回False
if not has_cycle:
return False
# 计算环的长度
cycle_length = 0
while True:
slow = slow.next
cycle_length += 1
if slow == fast:
break
return cycle_length
在这个方法中,我们使用快慢指针找到相遇点,然后通过移动慢指针并记录步数的方式计算环的长度。如果链表中不存在环,则直接返回False。
这种方法的时间复杂度为O(n),其中n是链表的长度。空间复杂度为O(1),因为我们只使用了两个指针来遍历链表,没有使用额外的数据结构。
哈希表
这种方法使用哈希表来记录每个节点的访问情况。具体步骤如下:
- 创建一个空的哈希表。
- 从链表的头节点开始,依次遍历链表中的每个节点。
- 检查当前节点是否已经在哈希表中存在,如果存在,则表示链表存在环,返回环的长度;否则,将当前节点加入到哈希表中。
- 如果遍历完整个链表都没有发现环,则返回False表示链表中不存在环。
以下是使用哈希表计算环的长度的代码示例:
def detectCycleLength(head):
visited = set()
node = head
cycle_length = 0
while node:
if node in visited:
return cycle_length
visited.add(node)
node = node.next
cycle_length += 1
return False
这种方法的时间复杂度为O(n),其中n是链表的长度。由于使用了哈希表来存储节点,空间复杂度也为O(n),因为最坏情况下需要存储整个链表的所有节点。