BM7 链表中环的入口结点
知识点链表哈希双指针
描述
给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。
数据范围: ,
要求:空间复杂度
,时间复杂度
例如,输入{1,2},{3,4,5}时,对应的环形链表如下图所示:
可以看到环的入口结点的结点值为3,所以返回结点值为3的结点。
输入描述:
输入分为2段,第一段是入环前的链表部分,第二段是链表环的部分,后台会根据第二段是否为空将这两段组装成一个无环或者有环单链表
返回值描述:
返回链表的环的入口结点即可,我们后台程序会打印这个结点对应的结点值;若没有,则返回对应编程语言的空结点即可。
示例1
输入:
{1,2},{3,4,5}
复制返回值:
3
复制说明:
返回环形链表入口结点,我们后台程序会打印该环形链表入口结点对应的结点值,即3
示例2
输入:
{1},{}
返回值:
"null"
说明:
没有环,返回对应编程语言的空结点,后台程序会打印"null"
示例3
输入:
{},{2}
返回值:
2
说明:
环的部分只有一个结点,所以返回该环形链表入口结点,后台程序打印该结点对应的结点值,即2
题解
先看代码,实现比较简单,稍后再做分析。
struct ListNode
{
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(nullptr)
{
}
ListNode() = default;
};
// 判断链表是否有环,如果有环返回快慢指针相遇的节点,否则返回空
ListNode *MeetingNode(ListNode *pHead)
{
if (pHead == nullptr || pHead->next == nullptr)
{
return nullptr;
}
auto slow = pHead;
auto fast = pHead;
while (fast != nullptr && fast->next != nullptr)
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
{
return slow;
}
}
return nullptr;
}
ListNode *EntryNodeOfLoop(ListNode *pHead)
{
// m 是快慢指针相遇的节点,这一步很容易理解
auto m = MeetingNode(pHead);
if (m == nullptr)
{
return nullptr;
}
// 这里的代码虽然简单,但是为什么将first指针和m指针分别向前移动,就一定会相遇呢?
// 并且为什么相遇的时候就一定是环的入口呢?稍后再做 分析
auto fisrt = pHead;
while (fisrt != m)
{
fisrt = fisrt->next;
m = m->next;
}
return m;
}
关于链表环的数学论证
假设有一个有环单链表,链表长度为a,并且环的长度为b。那么使用快慢指针移动后会有一个相遇的节点,假设其索引为i,索引从1开始。那么i的值一定是b+1。将2个指针分别指向头结点和b+1索引的节点,将他们分别移动a - b个节点,则他们一定会在环的入口节点处相遇。
快慢指针相遇索引位置的证明
已知链表长度为a,环的长度为b,链表的起始索引为1,让快慢指针都从1的所引处出发,假设他们相遇的位置为i,那么有:
- 1. 慢指针已经走了i-1步
- 2. 快指针已经走了(i-1)*2步
可得:(i-1)*2 = (a-1) + (i - (a - b))
最终求得:i = b + 1
解释:
- (i-1)*2为快指针所走的步数
- a-1为从索引1走到链表末尾的步数
- i - (a-b)为快指针从末尾处走到i的步数
基于以上原因最终有上面的等式。
两个指针从索引1和索引b+1处出发,他们一定会相遇,且相遇的位置就是链表环的入口的证明
由于环的长度为b,链表长度为a,索引起始位置为1,则环的入口索引为a - b + 1
从索引为1的地方走到环的入口需要走a-b步,从b+1处出发也走a-b步,得到的结果为:
b + 1 + a - b = a + 1,此时指向的节点应该是索引为a的节点的下一个节点。
由于链表有环,因此链表的最后一个节点的下一个节点就是指向的环的入口节点。
证明完毕~~