前言:本节内容主要是讲解链表的两个问题 :1、判断链表是否带环; 2、一个链表有环, 找到环的入口点。 本节内容适合正在学习链表或者链表基础薄弱的友友们哦。
我们先将问题抛出来,友友们可以自己去力扣或者牛客网去找相应题目, 这里直接贴链接:(没有做过这两个题的友友 千万! 千万! 千万! 要先自己做一下这两个题。)
判断链表是否带环:141. 环形链表 - 力扣(LeetCode)
带环链表的入环节点:LCR 022. 环形链表 II - 力扣(LeetCode)
目录
我们先来讲解第一道题
判断链表是否带环
题目解析
题目:
代码框:
题目非常的简单, 就是要求我们设计一个算法, 判断这个链表中是否右带环结构就可以了。 如果有带环结构, 那么就返回true, 如果没有带环结构, 那么就返回false。
算法原理
算法演示
解决这个问题需要用到快慢双指针算法, 我们利用题中所给示例进行演示:
先定义两个指针slow, fast。并且slow和fast要指向同时指向头节点, 否则在第二道题的时候处理起来会变的复杂。(具体为什么会变得复杂请友友们现在不要深究, 第二题的时候会讲到的, 到时再思考就行啦)
然后, 我们向后进行遍历, 遍历的过程是这样的: slow指针一次向后移动一个节点。 fast一次向后移动两个节点。当两个节点相遇的时候就说明我们的链表是带环的。
而如果我们的fast指向了空节点, 那么就说明我们的链表是不带环的。
我们演示一遍是这样的:
代码贴图如下:
bool hasCycle(struct ListNode *head)
{
//先判断下链表为空的情况
if (head == NULL) return false;
//1、创建两个指针, slow, fast同时指向链表头节点。
struct ListNode* slow = head;
struct ListNode* fast = head;
//2、遍历整个链表, 判断是否有环
while (fast != NULL && fast->next != NULL)
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast) return true; //如果两个指针指向同一个位置, 说明有环。
}
return false; //退出循环说明无环。
}
这就是算法的基本思路, 我们接下来进行剖析这个算法:
算法原理
为了证明我们的结论的普遍性, 我们利用上面的抽象图来进行演示。
这里的环周长就相当于C个节点, 前面的直线L就相当于没有进入环之前的L个节点。 然后slow每次走一个节点, fast一次走两个节点。
fast走的比slow要快, 所以, fast一定是在slow前面的。如果没有环的话, fast是肯定会先一步指向空的。 fast和slow也就无法相遇了。 所以如果fast指向空, 那么就一定没有环。
但是如果有环, 这个时候fast一定会先进入环。如图所示:
然后继续遍历, slow再进入环。如图所示:
那么, 重点就来了。 slow进入环之后, 如果fast和slow再继续遍历, 那么是不是fast和slow之间的距离在不断的缩小, 是不是就相当于fast在追击slow。 我们假设fast到slow的距离为N, 此时我们就将问题转化为了一个追击相遇问题。
fast每次走两步, slow每次走一步。 那么每回合fast和slow之间的距离就减少1。 循环下来就是N - 1 - 1 - 1 - 1 - 1, 直到N为零位置。 此时fast和slow相遇。并且当这两个指针相遇的时候, 只有两种情况, 一个是在环的入口点相遇, 一个是在环的其他位置相遇。 但这两种情况可以归到一类里面——slow和fast会在环内相遇。
综上, slow和fast只要相遇了, 他们就会在环内, 说明链表有环。
然后到这里这个题的算法原理基本结束了, 但是, 这里还有一个经常考的探究性问题, 很重要的扩展知识。 接下来进行分析:
原理扩展
从上文我们知道, 当slow指针和fast指针相遇的时候一定再环内。 但是有没有可能slow和fast不会相遇, 每次fast指针都越过slow指针, 导致两个指针永远无法相遇?
答案是我们前面分析的fast走两步, slow的情况不会。
但是如果fast一次走三步, slow一次走一步以及一些其他情况就可能fast直接越过slow, 永远不会相遇。这里需要具体情况具体分析。
分析过程如下:
如果我们slow每回合走一步, fast每回合走三步。 那么fast和slow的步数就相差2。 假设slow进环的时候slow和fast之间的距离相差N。那么循环下来就是N - 2 - 2 - 2. 这里如果N是偶数, N就恰好减少到零了, 这个时候slow和fast指针就相遇了。
但是如果N是奇数呢? N - 2 - 2 - 2就有可能得到-1,我们假设圆环的长度为C,这个时候就是如图这种情况:
那么, fast和slow之间的距离此时变成了C - 1。
接下来再进行分类讨论, 如果C为奇数, 那么C - 1就为偶数, 这样 C - 1 - 2 - 2 - 2……就有可能减少到零; 如果C为偶数, 那么C - 1就是奇数, 这样 C - 1 - 2 - 2 ……就会重新减少到-1, 然后fast和slow之间的距离又变成C - 1, 这个时候就陷入了死循环。
所以,综上我们可以得出小结论: 当fast一次走三步, slow一次走一步。如果N为偶数, 那么fast和slow一定相遇。 如果N为奇数, C为奇数。 那么fast和slow也会相遇。 如果N为奇数, C为偶数, 那么fast和slow永远不会相遇。
将上面的推导过程总结为一个算数表达式就是 : (N + x * C)% 2 ? 0;//距离N + x圈后模上fast和slow步数差是不是等于0.
所以, 我们得出的大结论就是:假设slow和fast每回合的步数相差sub.进入环的时候fast指针与slow指针之间的距离为N。圆环的长度为C。如果有 : (N + x * C)% sub == 0。就说明fast和slow会相遇, 否则不会相遇。
以上就是本道题的所有知识点。
ps:代码中的细节问题,属于代码编写的范畴, 不属于算法原理。 本篇内容只讲算法原理。 代码友友们自行编写调试。
环形链表的入口节点
题目解析
题目:
代码框:
这道题就是上面那道题的提高版本。 需要先对链表判断是否有环, 然后再判断入环节点。
算法原理
要找到环的入口点同样是有结论的, 这里我们先用结论进行代码的展示。 再进行分析
算法演示
寻找环的入口点的算法就是先利用上面一题的方法先判断是否存在环。 这个时候如果存在环的话fast指针和slow指针会在环内的某一个点相遇。
然后,我们定义一个meet指针指向这个相遇节点。 然后再重新定义一个指针指向链表的头节点。 让meet指针和指向头节点的指针同时向后遍历。 最后相遇的节点就是我们环的入口节点。 (原理是数学证明, 后面会进行证明。)
如图为代码贴图:
struct ListNode *detectCycle(struct ListNode *head)
{
//1、定义快慢指针
struct ListNode* slow = head;
struct ListNode* fast = head;
//2、循环判断是否有环
while (fast != NULL && fast->next != NULL)
{
//fast走两步, slow走一步。
slow = slow->next;
fast = fast->next->next;
//如果相遇, 说明有环。 然后寻找入环节点
if (slow == fast)
{
//meet节点指向fast和slow相遇节点。
struct ListNode* meet = slow;
//让slow重新指向头节点
slow = head;
//如果两个指针不相等, 就让他们向后遍历。
while (slow != meet)
{
slow = slow->next;
meet = meet->next;
}
//最后返回相遇节点
return meet;
}
}
return NULL; //从循环中出来说明fast走向了空, 说明没有环。
}
算法原理
要证明为什么从相遇位置和头节点的两个指针同时向后遍历, 相遇节点就是入环节点。我先给一张抽象图, 方便观察与理解:
假设我们从图中时刻开始向后遍历。 首先, fast先进环。如下图:
然后, fast继续向后走。 一直到slow进环:
好, 在这里停住, 这里有很重要的问题。 就是这个fast此时在环中已经走了几圈?
这个圈数确定吗?答案是不确定。 因为我们并不知道这个圈的大小, 如果这个圈很小很小。 然后前面的直链很长, 那么从fast进环到slow进环这一段时间中fast就可能在环中转了很多很多圈。
我在这画出一个例子就好懂了:
从这个例子我们可以看出, 在fast到slow这段时间内, fast在环中走的圈数是不确定的。
知道了这点后, 我们继续向下遍历, 一直到slow和fast相遇。
现在, 另外一个重要的问题就是:从slow进环到被fast追上, slow转了几圈?
我们看这样一个例子:
如果当slow进环的时候, fast恰好在slow前面一个位置。 如图:
那么到fast追上slow的时候, slow转的了一圈吗?
我们利用方程算一下:假设slow行走的路程是s, 那么fast就是2s。 此时就有:2s - s = C - 1;
得到的就是s == C - 1; 很显然就算在这种极端情况下slow都没有走一圈, 那么其他情况下更不可能走一圈。 那么上面的问题的答案就是: 从slow进环到被fast追上, slow一圈也没有转。
有了这些的铺垫后。 我们再从整体出发看 : 从两个指针开始遍历到两个指针相遇。 设slow行走的距离是X。 那么fast指针行走的距离就是2 * X; 我们设从入环到节点到slow和fast相遇的位置的距离为N。如图:
那么就有N + L = X;所以slow行走的距离就是N + L;fast行走的距离就是2 *(N + L);
但是, 对于fast来说, 还有另外一个式子: fast在从入环到slow入环期间,我们分析过了。 fast可能行走了几圈。 我们设为k圈。 然后从入环节点到相遇位置为N。 那么就有了fast的行走距离又可以有 : L + k * C + N;
那么就有 2 *(N + L)== L + k * C + N。
计算后就是k* C - N == L; 也可以写成 : (k - 1) * C + (C - N) == L;
而我们的相遇节点到入环节点的距离恰好是 C - N; 所以,根据这个式子。 我们就可以证明如果从相遇节点和头节点同时向后遍历, 那么最后再次相遇时的节点就是入环节点。
现在我们来思考这么一个问题。 如果fast指针和slow指针一开始没有从一点出发。 那么他们的相遇节点, 这个位置出发的meet指针和从头节点出发的指针还能在环的入口点相遇吗?
假设fast在slow的下一个节点出发如图:
那么我们可以重新使用方程推导一下: 从开始到两指针相遇,slow的行走距离还是L + N, 那么fast就有 2 * (L + N)。
而fast从开始到进环, 行走的距离变成了L - 1; 从环到相遇的这段距离也是不变的, 同样是N; 而且在环中转的圈数不确定, 设K * C; 那么fast的行走距离就有 : L - 1 + K * C + N;
就可以得到 : 2 * (L + N) == L - 1 + K * C + N;
化简后就是 :( K - 1) * C + C - N == L + 1;
由此可以推断出结论 : 从meet节点出发的指针和从链表头节点出发的指针并不能相遇。
---------------------------------------------------------------------------------------------------------------------------------
以上, 就是本节的全部内容。