1.题目描述
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。不允许修改 链表。
示例1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例2:
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例3:
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。
来源:力扣(LeetCode)
2.题目分析
2.1 哈希集合辅助法
思想比较简单,就是依次遍历链表,每次遍历到一个元素先检查这个节点是否已经遍历过,如果已经遍历过就说明碰到的环,又因为遍历是从前到后的,所以第一次遇到的已经存在的节点肯定就是环的起点,此时返回结果退出程序。如果该节点不存在于哈希表中,说明还没碰到环,那么继续遍历下一个节点知道为空。
2.2 快慢指针
2.2.1 快慢指针的工作
- 一般我们使用的快慢指针都是二倍速的,这么说吧,如果有小敏和小明两个同学,小明是个喜欢运动的同学所以跑的比较快,有多快呢,他的速度是小敏的两倍,假如他俩一起在操场跑圈的话那么他们肯定在某个时间点相遇。类比到指针也是一样的,如果我们设置两个快和慢的指针
fPtr
和sPtr
指向链表头部,如果链表存在环,那么它们一定会再次相遇的。在这道题中我们要求的就是环的起点。讲个小故事:
小明和小敏在晚饭后同时从食堂出发跑到操场跑圈,从一开始小明就是以小敏的二倍速开始跑,小敏到操场之后跑了小半圈又发现小明跟她相遇了,觉得这个人长得还挺帅于是跟他打招呼,说道:“好巧啊,我们又碰到了”,小明是个钢铁直男,严肃地说道:“这有什么巧的,我比你跑的快两倍”,此时我们又在绕着一个圈跑,那肯定会遇到啊”。小敏略感无语,不知道怎么回答他,然后小明继续说道,”你看你从食堂到这里跑的路程是
s1
,我现在跑的路程是s2
,但是这里的s1*2 = s2
”。小敏明显感觉有点不适,但是小明还是自顾自地说道:“不要小瞧这个呀,这里面有奇妙的数学知识呢”。小敏不耐烦地问道:“那是什么呢?”。小明回答道:“你看假如从食堂到操场的距离为a
,操场一圈是c
,而你在操场现在第一圈已经跑了b
的距离,然后我是比你多跑一圈但是我跑的总距离是你的两倍,这时候就有2(a+b) = a+b+c
;进而就会得出a+b=c
是不是感觉很有规律呢?”。小敏感觉这是个钢铁直男不再跟他说话了。小明又兴奋地说道:“其实还可以根据这个算出操场的入口地点呢”。小敏感觉很奇怪,入口地点不就在那里吗,还用求嘛?但是小明明显是陷入数学等式,又自顾自地说到,此时我们得出了a+b=c
,说明你现在距离操场入口的距离是c-b=a
,跟食堂到这里的距离一样呢,所以找个人跟你以同样的速度从食堂开始跑到这里,你们相遇的地方就是操场入口。
回到问题本身,如果使用快慢指针的话我们可以明确的确定链表是否有环,但是如何确定环的起点呢?我们将上面的例子归化到链表中来分析,假设有下面的链表
- 我们分别设置一个快慢指针
fPtr
和sPtr
指向节点6,让他们分别每次向前两个和一个节点。我们会发现它们的第一次相遇总是会在环内的。若相遇,此时的快指针fPtr
一定比慢指针sPtr
多走了一个环的长度c
,设原点到环口要a
步,从环口到相遇要b
步,则相遇时sPtr
走了a+b
步,fPtr
走了2*(a+b)
步也就是a+b+c
步,所以有2(a+b) = a+b+c
,那么我们就可以得出a+b=c
。
- 此时根据上面的式子,我们发现
b
这个点很神奇,它距离环入口的距离就是原点到环入口的距离。用数学等式证明就是,设从相遇位置到环口为d,则b+d=c,则a=d
,这时候要求a,就让一个指针从起点
出发,另一个相遇位置
出发,再次相遇就是环口。
3.题目解答
3.1 哈希集合辅助求解
/**
* 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* cur = head;
while(cur!=nullptr){
if(visited.count(cur)==0){
visited.insert(cur);
}
else return cur;
cur = cur->next;
}
return nullptr;
}
};
3.2 快慢指针
ListNode *detectCycle(ListNode *head) {
if(head == nullptr || head->next == nullptr){
return nullptr;
}
//分别设置慢指针快指针
ListNode* sPtr = head;
ListNode* fPtr = head;
while(fPtr != nullptr && fPtr->next!=nullptr){
sPtr = sPtr->next;
fPtr = fPtr->next->next;
//若快慢指针相遇则说明有环
//条件b+d=c=a+b
while(sPtr == fPtr){
ListNode* cur1 = fPtr;
ListNode* cur2 = head;
//根据数学条件得出相遇点到环入口的距离等于起点到入口的距离
while(cur1 != cur2){
cur1=cur1->next;
cur2=cur2->next;
}
//再次相遇则为入口
return cur2;
}
}
return nullptr;
}
总结:快慢指针