解法一:双指针法
思路
题目要求返回链表开始入环的第一个节点,无环则返回NULL。
首先判断链表有没有环,采用快慢指针法。fast和slow指针都从head出发(注意:在单纯判断链表有没有环的问题中,slow从head出发,fast从head->next出发,是为了使用while (fast == slow)而不必用do while循环来判断相遇条件。而本题一定要都从head出发才能进行后续相遇条件的推导),fast每次走2步,slow每次走1步,若有环,fast最终会和slow相遇;若无环,fast最终会走到链表尾部,走到链表尾部可以用(fast->next == nullptr || fast->next->next == nullptr)来判断,也可以用(fast == nullptr || fast->next == nullptr)来判断,考虑到给定的链表可能出现空链表的情况,故选择用第二种条件进行判断。无环返回nullptr即可。
若fast和slow相遇,说明链表有环,接下来找出链表尾入环的第一个节点即可。首先分析快慢指针相遇时两个指针分别走过的步数。
如上图,假设有环链表,环外的节点数为a,环内节点数为b(含入环处节点)。假设相遇时fast指针走过的节点数为f,slow指针走过的节点数为s,那么:
f = 2s; —— fast指针每次比slow指针快1倍,总路程是2倍的关系
f = s + nb; —— 相遇时一定是在环内,且fast指针比slow指针多走n圈才能追上
由上式可得 s = nb,f = 2nb。
对于slow指针而言,每次走到环入口处节点时,走过的路程为 a + nb。而fast和slow相遇时,slow走过了nb,说明从相遇时起,slow再走a步就能走到环入口处。一个从head出发的新指针,每次走1步,走a次便会和slow相遇。因此在slow和fast相遇时,再引入新的指针tmp从head出发,tmp和slow相遇时,tmp和slow指向的节点即为环入口处节点。
步骤
1. 初始化slow和fast: ListNode* slow = head; ListNode* fast = head;
2. do {} while (slow != fast); 循环判断链表是否有环,若有则找到slow和fast相遇的点;若无,返回NULL
(1)如果fast走到链表尾部说明无环:if (fast == nullptr || fast->next == nullptr) return nullptr;
(2)slow每次走1步,fast每次走2步: slow = slow->next; fast = fast->next->next;
3. 循环跳出,找到相遇的点后,引入指向head的新指针tmp:ListNode* tmp = head;
4. while循环找到tmp和slow相遇的点:
while (tmp != slow) {
tmp = tmp->next;
slow = slow->next;
}
5. tmp、slow相遇,返回tmp指向的节点。 return tmp;
代码
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* slow = head;
ListNode* fast = head;
do {
if (fast == nullptr || fast->next == nullptr) return nullptr;
slow = slow->next;
fast = fast->next->next;
} while (slow != fast);
ListNode* tmp = head;
while (tmp != slow) {
tmp = tmp->next;
slow = slow->next;
}
return tmp;
}
};
分析
时间复杂度:O(N)
在第一个do while循环中,相遇时slow指针走过的步数是小于a+b的,第二个while循环中,slow走的步数是a,也是小于a+b的,因此是线性的时间复杂度。
空间复杂度:O(1)
双指针使用常数个额外空间。
解法二:哈希表
思路
遍历给定链表,若遇到之前遍历过的节点,说明链表中有环,否则无环。
步骤
1. 创建哈希表,用来存储遍历过的节点:unordered_set<ListNode*> visited
2. 初始化指针ptr指向head: ListNode* ptr = head;
2. while循环遍历链表,循环终止条件为ptr指向链表尾:while (ptr != nullptr) {}
(1) 若在哈希表中找到当前遍历的节点,则返回该节点: if (visited.count(ptr)) return ptr;
(2) 否则将当前节点加入到哈希表中: visited.insert(ptr);
(3) 然后继续向后遍历: ptr = ptr->next;
3. 循环跳出说明遍历完链表都没找到曾经遍历过的节点,直接return nullptr。
代码
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
unordered_set<ListNode*> visited;
ListNode* ptr = head;
while (ptr != nullptr) {
if (visited.count(ptr)) return ptr;
visited.insert(ptr);
ptr = ptr->next;
}
return nullptr;
}
};
分析
时间复杂度:O(N)
空间复杂度:O(N)