目录
环是链表中的经典问题,问题确定是否存在环并不算难,确定环的入口会比较复杂,但这两个一般都会同时出现,所以,两个都必须掌握。
1.如何确定链表中存在环
leetcode力扣 141https://leetcode.cn/problems/linked-list-cycle/description/题目:给你一个单链表的头结点,判断链表中是否有环。
示例: 输入:head=
输出:true
方法一:Hash
最容易想到的方法是遍历所有节点,每次遍历到一个节点时,判断该节点此前是否被访问过。
具体地,我们可以使用哈希表来存储所有已经访问过的节点。每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。若存在环,一定会发生碰撞,发生碰撞的位置也就是环的入口,即上图node(2)。
但这还不够优秀,如何使用O(1)的空间完成呢?见下一个方法
方法二:双指针法
使用快慢指针fast,slow,两者一开始都指向第一个结点,使用while循环,fast一次走两步,slow一次走一步,循环判断条件为while(fast !=NULL &&fast->next !=NULL),
如果没有环,那么fast指针一定会在某时刻指向NULL或fast的下一个结点为NULL。则退出循环。
若存在环,那么fast上面的两种情况一定不会发生,但是,随着时间的推移,fast与slow指针一定会在某时刻相遇,所以在循环体内部增加if判断。也许你会问,fast一次走两步,刚刚好跳过slow了怎么办,下面我们画图分析
在fast接近slow时有以下两种情况:
情况1:
fast在node(1)与slow在node(2),相差1步,这时slow走一步,fast走两步,刚好在node(3)相遇,退出循环。
情况2:
fast在node(1)与slow在node(3),相差2步,这时slow走一步,fast走两步,就变成了上面的情况1,fast与slow还是会相遇,退出循环。
所以,若存在环,使用快慢指针法,快慢指针一定会相遇。
bool hasCycle(struct ListNode *head) {
if(head == NULL || head->next == NULL) return NULL;
struct ListNode* slow = head;
struct ListNode* fast = head;
slow=slow->next;
fast=fast->next->next;
//若fast遍历至链表尾,则说明不存在环
while(fast !=NULL &&fast->next !=NULL){
//若fast与slow相遇,则说明存在环
if(fast == slow){
return true;
}
slow=slow->next;
fast=fast->next->next;
}
return false;
}
2.若存在环,如何确定环的入口
leetcode力扣 142https://leetcode.cn/problems/linked-list-cycle-ii/description/ 确定环入口也是使用快慢指针法,不过是在fast与slow相交后,使slow重新指向头结点,然后两个指针,同时开始一次走一步遍历,最后,两指针再次相交的结点就是环的入口。为什么呢?下面来分析
不难知道,在两指针第一次相交时,fast肯定已经在环中饶了n圈了。下面来分情况画图分析
x为head,y为环入口,z为fast与slow第一次相交的结点,A,B,C分别为xy,yz,zy的距离。
若n=1,也即fast已经在环中饶了一圈了,且fast的速度使slow的两倍,所以A+B+C+B = 2(A+B) 即 A=C,所以,第一次相遇后,slow重新指向头结点两指针都一次走一步,最终会在y结点相遇。
若n>1,也即fast已经在环中饶了n圈了,有 A+N(B+C)+B = 2(A+B),即 A=N(B+C)-B,B+C=length=环的长度,所以 A=N*length-B,所以第一次相遇后,slow重新指向头结点两指针都一次走一步,最终会在y结点相遇。
综上所述,快慢指针第二次相交的结点就是环的入口。
struct ListNode *detectCycle(ListNode *head) {
if(head == NULL || head->next == NULL) return NULL;
struct ListNode* slow = head;
struct ListNode* fast = head;
slow=slow->next;
fast=fast->next->next;
//若fast遍历至链表尾,则说明不存在环
while(fast !=NULL &&fast->next !=NULL){
//若fast与slow相遇,则说明存在环,开始找环入口
if(fast == slow){
slow=head;
while(fast!=slow){
fast=fast->next;
slow=slow->next;
}
//返回环入口
return fast;
}
slow=slow->next;
fast=fast->next->next;
}
return NULL;
}
总结:
1.确定链表中是否存在环,使用双指针快慢指针法,若存在环,则两指针一定会相遇。
2.如何确定环的入口,先确定链表中有环,然后将slow重新指向head,两指针同时一次走一步开始遍历,最后两指针再次相遇的结点就是环的入口了。