快慢指针算法(Floyd's Tortoise and Hare Algorithm)是解决链表和数组中环检测问题的经典方法。这个算法的核心思想是使用两个指针,一个移动速度快(通常每次移动两步),一个移动速度慢(通常每次移动一步),通过它们不同的移动速度来检测环的存在并找到环的入口点。
在实际应用中,快慢指针算法主要解决两类问题:
提示:虽然算法原理相同,但在链表和数组这两种不同数据结构中应用时,指针重置的逻辑会有所不同,这是很多初学者容易混淆的地方。
为了更好地理解算法,我们需要先定义几个关键变量:
当快慢指针第一次相遇时,它们走过的路程满足以下关系:
由于快指针的速度是慢指针的两倍,所以有:
a + b + kc = 2(a + b)
简化后得到关键公式:
a = k*c - b
这个公式告诉我们,从起点到环入口的距离a,等于快指针绕环k圈的距离减去相遇点到环入口的距离b。
找到相遇点后,我们需要重置其中一个指针到起点,然后让两个指针以相同速度前进,最终它们会在环入口处相遇。这是因为:
如果我们将一个指针重置到起点,另一个留在相遇点,然后以相同速度移动:
LeetCode 142题要求在一个可能有环的链表中找到环的入口节点。这个场景的特征包括:
在这种场景下,我们需要在第一次相遇后重置快指针到头节点,而不是慢指针。原因如下:
如果错误地重置慢指针,数学关系将不成立,算法会失效。
python复制class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def detectCycle(self, head: ListNode) -> ListNode:
if not head or not head.next:
return None
# 第一阶段:检测环
slow = fast = head
has_cycle = False
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
has_cycle = True
break
if not has_cycle:
return None
# 第二阶段:找环入口
fast = head # 关键:重置快指针
while slow != fast:
slow = slow.next
fast = fast.next
return fast
注意事项:
- 必须先检查链表是否为空或只有一个节点
- 快指针移动时要先检查fast.next是否存在,避免空指针异常
- 重置的是快指针,不是慢指针
LeetCode 287题要求在一个包含n+1个整数的数组中找到唯一的重复数,数组中的整数都在1到n之间。这个场景的特征包括:
在这种场景下,我们需要在第一次相遇后重置慢指针到起点(nums[0]),而不是快指针。原因如下:
python复制def findDuplicate(nums):
# 第一阶段:检测环
slow = fast = nums[0]
while True:
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast:
break
# 第二阶段:找环入口(重复数)
slow = nums[0] # 关键:重置慢指针
while slow != fast:
slow = nums[slow]
fast = nums[fast]
return slow
注意事项:
- 数组中的值必须在1到n之间,且只有一个重复数
- 移动指针时使用值作为索引
- 重置的是慢指针,不是快指针
- 这种方法不修改原数组,时间复杂度O(n),空间复杂度O(1)
| 对比维度 | 数组找重复数(LC287) | 链表找环入口(LC142) |
|---|---|---|
| 数据结构 | 数组 | 链表 |
| 指针类型 | 值索引 | 节点引用 |
| 初始位置 | nums[0] | head |
| 移动规则 | slow = nums[slow] | slow = slow.next |
| fast = nums[nums[fast]] | fast = fast.next.next | |
| 重置对象 | 慢指针(到nums[0]) | 快指针(到head) |
| 数学依据 | 满足a = k*c - b | 满足a = k*c - b |
通过对比两种场景,我们可以总结出选择重置指针的通用原则:
看初始位置:哪个指针是从起点开始移动的
看移动规则:指针如何前进
数学验证:确保重置后的移动满足a = k*c - b的关系
可能原因:
解决方案:
在实际编码中,我们不需要显式计算k的值。数学关系a = k*c - b已经包含了k,算法会自动处理这个关系。
这是为了保证:
标准的快慢指针算法只适用于有且只有一个重复数的情况。如果有多个重复数,需要采用其他方法,如二分查找或位运算。
这是快慢指针算法最简单的应用,只需要检测快慢指针是否会相遇,不需要找环入口。
python复制def hasCycle(head):
if not head or not head.next:
return False
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
找到环入口后,可以让一个指针固定,另一个指针移动,直到再次相遇,统计步数。
python复制def cycleLength(slow, fast):
length = 1
fast = fast.next
while slow != fast:
fast = fast.next
length += 1
return length
快慢指针也可以用来找到链表的中间节点(快指针速度是慢指针的两倍,当快指针到达末尾时,慢指针就在中间)。
python复制def middleNode(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
总体时间复杂度:O(n)
算法只使用了固定数量的指针变量,没有使用额外的数据结构。
空间复杂度:O(1)
快慢指针算法不仅用于面试题,在实际工程中也有广泛应用:
快慢指针算法体现了几个重要的算法设计思想:
掌握这些思想可以帮助我们解决更多类似的算法问题。