给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
提示:
- 链表中节点的数目范围是
[0, 104] -105 <= Node.val <= 105pos为-1或者链表中的一个 有效索引 。
**进阶:**你能用 O(1)(即,常量)内存解决此问题吗?
1. 算法核心思想:Floyd 判圈算法
判断链表是否有环的经典方法是“快慢指针法”(类似于操场跑道上的追及问题)。 设置两个指针,起点都在链表头部。慢指针(slow)每次走一步,快指针(fast)每次走两步。
- 如果链表没有环:快指针会率先到达链表的尾部(
NULL)。 - 如果链表有环:快指针会先进入环内绕圈。当慢指针也进入环内后,由于快指针的速度比慢指针快 1,快指针最终一定会追上并与慢指针重合。
2. 逻辑拆解
- 起步错位处理: 由于初始化时
slow和fast都指向head,如果直接将slow != fast写在while循环条件中,循环一开始就不会执行。因此,代码在进入循环前,先手动让fast走两步、slow走一步。在这个过程中,顺便处理了空链表或单节点链表的边界情况(直接返回false)。 - 循环追击:
while循环的条件严格限制了指针的安全运行范围(fast != NULL && fast->next != NULL),防止在步进过程中发生空指针越界访问。在循环体内,严格保持慢指针单步、快指针双步的移动频率。 - 状态识别与返回: 当
while循环结束时,只有两种可能的情况: - 快指针触碰到了链表边界(
fast == NULL或fast->next == NULL),这表明链表存在物理终点,直接返回false。 - 上述条件不成立,说明循环是因为
slow == fast被打破的,即快慢指针在环内相遇,返回true。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode* slow = head;
ListNode* fast = head;
if (fast!= NULL && fast->next != NULL) {
fast = fast->next->next;
}
else {
return false;
}
slow = slow->next;
while (slow != fast && fast != NULL && fast->next != NULL) {
slow = slow->next;
if (fast->next != NULL) {
fast = fast->next->next;
}
else {
fast = fast->next;
}
}
if (fast == NULL || fast->next == NULL) {
return false;
}
return true;
}
};
3. 复杂度分析
- 时间复杂度:O(N)。其中 N 是链表中的节点数。
- 当链表无环时,快指针走完一半的节点数即可到达末尾,时间开销为线性。
- 当链表有环时,慢指针进入环后,快指针追赶慢指针所需的相对距离不会超过环的长度。整体循环次数依然与节点总数成正比。
- 空间复杂度:O(1)。算法仅维护了
slow和fast两个节点指针,未申请任何与数据规模相关的额外内存空间。
