🕒 题目 Ⅰ
给你一个链表的头节点 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 <= 105
- pos 为 -1 或者链表中的一个 有效索引 。
进阶:你能用 O(1) 内存解决此问题吗?
⌛ 方法① - 快慢指针
💡 思路:定义两个指针 fast
和 slow
,slow
指针每次移动一步,而fast
指针每次移动两步。如果链表带环,二者最终会在链表的某个节点相遇。
代码实现如下:
bool hasCycle(struct ListNode *head) {
struct ListNode* fast = head, *slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(fast == slow) //如果快指针与慢指针相遇就代表有环
return true;
}
return false; //如果循环可以结束就代表没环
}
🕒 面试题
1、为什么快指针每次走两步,慢指针每次走一步,二者最终一定会在环中相遇?
解答:假设slow进环之后,fast和slow之间的差距为N(即追赶距离)。每一次追逐的过程中,距离都会缩小1,过程为:N、N-1、N-2、N-3、…、1、0,距离为0的时候就是相遇。
2、快指针一次走三步,慢指针一次走一步,可不可以?
解答:不一定,还是假设进环之后fast和slow之间的差距为N,每追逐一次,之间的距离缩小2步,过程为:N、N-2、N-4、N-6、…,对于这样的结果,如果之间的差距为偶数,最终之间的距离会变为0,说明追上了;如果之间的差距为奇数,最终之间的距离会变成1。然后继续追逐下去,差距就变成了-1,意味着fast和slow之间的距离变成了C-1,C为环的长度,然后重新追逐。
如果C-1是偶数,再追一圈就可以追上,如果C-1是奇数,永远追不上,一直进入新的追逐。
3、快指针一次走X步,慢指针一次走Y步可不可以?
解答:快指针一次走X步,慢指针一次走Y步,慢指针入环时二者的距离为N,快指针与慢指针之间的距离一次缩小X-Y,如果X-Y为1,则一定能够相遇;如果X-Y大于1,如2,3,4… … 可能相遇,也可能不相遇。
🕒 题目 Ⅱ
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。不允许修改 链表。
示例同上题,输出是返回索引为 pos值 的链表节点。
⌛ 方法① - 快慢指针
💡 思路:定义两个指针 fast
和 slow
,slow
指针每次移动一步,而fast
指针每次移动两步。首先求出二者在环中的相遇点;然后一个指针从链表的头开始走,另一个指针从相遇点开始走,最终二者会在入环点相遇。
假设环的长度为C,环之前的节点的长度为L,slow与fast在环中X距离处相遇,相遇时快指针已经在环中走了N圈,则如下图所示:
- slow走的距离: L + X L+X L+X
- fast走的距离: L + X + N × C L+X+N×C L+X+N×C
- fast走的距离是slow的两倍:
2
×
(
L
+
X
)
=
L
+
X
+
N
×
C
2×(L+X)=L+X+N×C
2×(L+X)=L+X+N×C
化简得: L + X = N × C L+X=N×C L+X=N×C
转化得: L = N × C − X = = > L = ( N − 1 ) × C + C − X L=N×C-X ==>L=(N-1)×C +C-X L=N×C−X==>L=(N−1)×C+C−X - 所以一个指针从相遇点开始走,另一个指针从头开始走,二者最终会在入环点相遇。
代码实现如下:
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode* fast = head, *slow = head, *cur = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
// 找到相遇的节点
if(fast == slow)
{
struct ListNode* meet = slow;
// 一个指针从头开始走,另一个指针从相遇点开始走
while(cur != meet)
{
cur = cur->next;
meet = meet->next;
}
return cur; // 二者最终会在入环点相遇
}
}
return NULL; // 无环
}
⌛ 方法② - 转换为链表相交问题
💡 思路:定义两个指针 fast
和 slow
,slow
指针每次移动一步,而fast
指针每次移动两步。首先求出二者在环中的相遇点,然后记录下相遇点的下一个节点并让相遇点的next
指向NULL,让相遇点的下一个节点作为新链表的头,与原链表求交点,最后再把相遇点的next复原。
注意:
- 我们需要将相遇点的next置空,否则在求两个链表的交点的时候会发生死循环;
- 由于题目要求不修改原链表,所以最后我们需要把相遇点的next复原;
代码实现如下:
// 求两个链表的交点
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
// 如果任意一个链表为空,则没有交点
if (headA == NULL || headB == NULL) {
return NULL;
}
struct ListNode *curA = headA;
struct ListNode *curB = headB;
// 当两个指针不同步时,继续循环
while (curA != curB) {
// 如果curA到达链表A的末尾,则重置到链表B的头部
curA = (curA == NULL) ? headB : curA->next;
// 如果curB到达链表B的末尾,则重置到链表A的头部
curB = (curB == NULL) ? headA : curB->next;
}
// curA和curB要么同时为空(没有交点),要么同时指向交点
return curA;
}
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode* slow = head, *fast = head, *cur = head;
while(fast && fast->next)
{
// 迭代
slow = slow->next;
fast = fast->next->next;
// 找到相遇点
if(slow == fast)
{
// 记录相遇点的下一个节点,并把链表从相遇点断开,避免求相交的时候发生死循环
struct ListNode* meet = fast;
struct ListNode* next = meet->next;
meet->next = NULL;
// 求两个链表相交
struct ListNode* intersection = getIntersectionNode(cur, next);
return intersection;
// 恢复原链表
meet->next = next;
}
}
return NULL; // 无环
}
❗ 转载请注明出处
作者:HinsCoder
博客链接:🔎 作者博客主页