环形链表
检测链表是否有环(快慢指针法)
1 | function hasCycle(head) { |
找到环的起始节点
1 | function detectCycleStart(head) { |
实际应用示例
1 | // 创建环形链表 |
环形链表在需要循环访问数据的场景中非常有用,如音乐播放列表、轮播图、资源轮询等。
环形链表检测算法详细解析
这段代码是用来检测链表中是否存在环,并找到环的起始节点的经典算法。它使用了”快慢指针”(Floyd’s Cycle-Finding Algorithm)的方法。下面我将详细解析每一部分的作用和原理。
算法整体思路
- 检测是否有环:使用快慢指针遍历链表,如果快指针追上慢指针,则存在环
- 找到环的起点:重置一个指针到链表头,然后两个指针同步前进,相遇点即为环的起点
代码逐行解析
第一部分:初始检查
1 | if (!head || !head.next) return null; |
- 检查链表是否为空或只有一个节点
- 这两种情况都不可能形成环,直接返回null
第二部分:初始化指针
1 | let slow = head; |
slow慢指针,每次前进一步fast快指针,每次前进两步hasCycle标记是否检测到环
第三部分:检测环的存在
1 | while (fast && fast.next) { |
- 循环条件:快指针及其下一个节点不为null(防止空指针异常)
- 慢指针前进一步,快指针前进两步
- 如果快慢指针相遇,说明存在环,设置标记并退出循环
第四部分:无环处理
1 | if (!hasCycle) return null; |
- 如果没有检测到环,直接返回null
第五部分:寻找环的起点
1 | slow = head; |
- 将慢指针重置到链表头部
- 快慢指针现在都每次前进一步
- 当它们再次相遇时,相遇点就是环的起点
- 返回这个节点
为什么这个算法有效?
数学原理
设:
- 链表非环部分长度为 L
- 环的长度为 C
- 快慢指针第一次相遇时,慢指针走了 S 步,快指针走了 2S 步
当快慢指针第一次相遇时:
- 快指针比慢指针多走了 n 圈环:
2S = S + nC⇒S = nC - 慢指针走的步数可以表示为:
S = L + a(a 是环内位置)
由 L + a = nC ⇒ L = nC - a
这意味着:从链表头到环起点的距离 L,等于从相遇点继续走 nC - a 步。这正是为什么第二次遍历时,从链表头和相遇点同时出发的两个指针会在环起点相遇。
为什么第二次遍历能找到起点?
- 第一次相遇后,将慢指针重置到头部
- 两个指针现在都每次走一步
- 当慢指针走了 L 步到达环起点时:
- 快指针从相遇点走了 L 步
- 因为
L = nC - a,所以快指针的位置是:a (原位置) + L = a + nC - a = nC - 即快指针也回到了环起点
时间复杂度分析
- 检测环:O(n) - 最多遍历整个链表一次
- 寻找起点:O(n) - 最多再遍历一次
- 总时间复杂度:O(n)
空间复杂度:O(1),只使用了固定数量的指针
示例演示
考虑链表:1 → 2 → 3 → 4 → 5 → 2(指向节点2形成环)
第一次遍历:
- 慢指针路径:1 → 2 → 3 → 4
- 快指针路径:1 → 3 → 5 → 3 → 5 → …
- 在节点4相遇
第二次遍历:
- 慢指针从头开始:1 → 2
- 快指针从4开始:4 → 5 → 2
- 在节点2相遇,这就是环的起点
这个算法巧妙利用了数学关系,通过两次遍历就能准确找到环的起点,是解决环形链表问题的经典方法。