题目:
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
Given a linked list, return the node where the cycle begins. If there is no cycle, return null
.
To represent a cycle in the given linked list, we use an integer pos
which represents the position (0-indexed) in the linked list where tail connects to. If pos
is -1
, then there is no cycle in the linked list.
Note: Do not modify the linked list.
Example 1:
Input: head = [3,2,0,-4], pos = 1
Output: tail connects to node index 1
Explanation: There is a cycle in the linked list, where tail connects to the second node.
Example 2:
Input: head = [1,2], pos = 0
Output: tail connects to node index 0
Explanation: There is a cycle in the linked list, where tail connects to the first node.
Example 3:
Input: head = [1], pos = -1
Output: no cycle
Explanation: There is no cycle in the linked list.
这道题,经典题型,某次随性面试居然面到这个,隐约中有印象是快慢指针但是具体忘了,结果在那儿硬想了半天想不到解决方案。刚刚仔细想了一下,发现自己没有思路的原因居然是,对这个链表有误解——我总觉得它可以打个转又继续往后,就像一根绳子一样,比如这样:_____O______。然而事实是,这特么就是一个普普通通的单向链表,只能有一个next啊!所以有环的情况下,就是在链表的最后,又拐了个弯回到了之前走到的某个节点,比如这样:_____O
想要求出环的入口节点,首先你得知道这个链表有没有环。怎么判断有没有环呢?快慢指针!快指针走两步,慢指针走一步,如果走着走着快指针指向了NULL,那完了,这个链表截止了,不可能有环了。那怎样才能早早判断出它有环,让我们跳出这个循环呢,那就等着快慢指针相遇了——它们总会在圈内相遇的,因为最后大家都在转圈圈。
解决了判断有没有环的问题,下一步要开始计算环的入口节点了,还是祭出快慢指针大法。假如已经知道了环的长度n,那么只需要让快指针先在链表上走n个节点,然后两个指针一起在链表上同速移动,当慢指针挪动到了环的入口的时候,快指针就在环里走了一圈回到了环的入口,两个指针就相遇了。这里刚开始不是很懂为什么如此奇妙,后来自己算了一下,假设链表的长度是m,那么入口节点是m-n,第二次慢指针走到入口就相当于走了m-n步,加上快指针第一次走的n步,最后就是走了m步,把整个链表都走完了并回到了环的入口节点。
那么接下来问题来了,我咋知道环有多长呢?那就只能在第一次判断有没有环的时候,快慢指针相遇的时候记录下相遇的节点,然后一步步挪,挪到回到之前记下的节点,这之间的步数就是环的长度了。
思路有了而且好像还算简单,但是写起代码来被各种bug气到爆炸了。纠结了好久好久好久好久,最后发现bug居然是在初始化的时候把p1和p2都设成了头节点,然后在立马接下来的while循环里的条件是while (p1 != p2),那么就根本没进这个循环啊……真是气哭了!解决这个问题以后似乎就变得顺利了…………另外又在讨论版里看到有其他人提出来的做法,是不需要求出环的长度就可以求入口节点的,下面摘录一下吧:
x为不带环的长度,c为环的长度,a为相遇时多走的环中的路。
第一次相遇时:
快指针走过的路程s1 = x + m * c + a
慢指针走过的路程s2 = x + n * c + a
而由于两个指针的时间相同、快指针是慢指针的两倍速度,则s1 = 2 * s2
即,x + m * c + a = 2 * (x + n * c) + a
化简得:x = (n - 2 * m )*c - a = (n - 2 *m -1 )*c + c - a
说明环前的路程是环的长度的k倍再多出橙色路段的距离。
于是,如果把一个指针赶回链表的头节点,再让两个指针同时同速开始挪动,当回到头节点的指针走了x,也就是到了入口节点时,还在环里的指针也多走了x = c - a,也回到了入口节点,于是两个指针在环的入口相遇了。
这个解释通过公式推导非常有理有据,也更容易理解。代码两种都写了,其中注释掉的部分是第二种方法的代码。LC上,时间12ms,99.33%;空间9.8M,41.85%。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};
*/
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead)
{
if (!pHead || !pHead->next || !pHead->next->next) {
return NULL;
}
ListNode* p1 = pHead->next->next;
ListNode* p2 = pHead->next;
// 确定是否有环
while (p1 != p2) {
if (p1->next && p1->next->next && p2->next) {
p1 = p1->next->next;
p2 = p2->next;
}
else {
return NULL;
}
}
/*p1 = pHead;
while (p1 != p2) {
p1 = p1->next;
p2 = p2->next;
}*/
// 求环的长度
int len = 1;
ListNode* meet_node = p1;
while (p1->next != meet_node) {
len++;
p1 = p1->next;
}
p1 = pHead;
p2 = pHead;
// 挪动p1 len步
for (int i = 0; i < len; i++) {
p1 = p1->next;
}
// 同时挪动p1、p2直到相遇
while (p1 != p2) {
p1 = p1->next;
p2 = p2->next;
}
return p1;
}
};
看了牛客的讨论版,发现还有一种需要修改原始链表的做法,叫做断链法。思路大概是每次经过一个节点,都把它和下一个节点断开来如下:
设置两个相邻的指针,姑且也叫它快慢指针吧,两个指针每次都往后挪一个节点,在挪动的同时也断掉两个指针之间的next,即让慢指针的next指向NULL,直到没有下一个节点为止,剩下的就是入口起点了。代码写起来还算好写,但按照自己的思路写的时候还是有点小毛病,刚开始写的时候while的循环条件是p1->next != NULL,也就是说当p1没有后续的时候,那么返回p1作为入口节点,但是这样会段错误,想了半天还是没怎么搞清原因。后来按照标准代码,也就是循环条件是p1 != NULL,最后返回p2,发现这样做的话把最后从尾节点返回去的那个next也断掉了,然后最后p1经过一个p1->next这个好像next已经不存在了?这个地方还是没太搞懂,留个坑以后慢慢填吧。代码:
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};
*/
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead)
{
if (!pHead || !pHead->next) {
return NULL;
}
ListNode* p1 = pHead->next;
ListNode* p2 = pHead;
while (p1) {
p2->next = NULL;
p2 = p1;
p1 = p1->next;
}
return p2;
}
};
2019.12.2补充:
重新写了一下这个代码,发现思路和之前完全不一样了,感觉现在写的更简洁了。先贴141的代码。
Runtime: 12 ms, faster than 73.63% of C++ online submissions for Linked List Cycle.
Memory Usage: 9.7 MB, less than 96.05% of C++ online submissions for Linked List Cycle.
/**
* 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* p1 = head;
ListNode* p2 = head;
while (p1 && p1->next) {
p1 = p1->next->next;
p2 = p2->next;
if (p1 == p2) {
return true;
}
}
return false;
}
};
142
Runtime: 20 ms, faster than 20.49% of C++ online submissions for Linked List Cycle II.
Memory Usage: 9.6 MB, less than 100.00% of C++ online submissions for Linked List Cycle II.
/**
* 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* p1 = head;
ListNode* p2 = head;
while (p1 && p1->next) {
p1 = p1->next->next;
p2 = p2->next;
if (p1 == p2) {
p1 = head;
while (p1 != p2) {
p1 = p1->next;
p2 = p2->next;
}
return p1;
}
}
return NULL;
}
};
2022.11.7
嗯,还是记得141的解法的,快慢指针嘛。但是代码写出来的不够简洁,刚开始想的先判断fast是否等于slow然后再挪,于是因为最开始都初始化成head了导致需要特殊处理,后来看了笔记才意识到可以换一下顺序解决……
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
return true;
}
}
return false;
}
}