前言:
在我们日常所见到的链表当中,极大多数是下面的这种结构:
通过遍历上述链表可以找到链表的尾部,而有一种链表结构它没有尾部,这种链表就叫做环形链表,示意图如下:
上述的链表没有尾部,相对于我们常见的链表而言,他的尾节点的 next 指针指向链表中的任意一个节点,也就在链表中形成了环形结构,下面我们来看一下如何判断链表是否存在带环结构。
分析:
带环链表的示意图:
解决思路一(相对速度为 1 时):
定义两个快(fast)、慢(slow)指针同时指向链表的头结点,快指针每次走两步(fast = fast->next->next),慢指针每次走一步(slow = slow->next),如果 快指针 在走的过程中能够追上 慢指针 ,那么此链表就是带环链表,下面是代码实现:
//链表中存在环则返回 true ,否则返回 false
bool hasCycle(struct ListNode *head) {
struct ListNode * fast = head;
struct ListNode * slow = head;
while(fast && fast->next) //当fast 或者 fast的next指针为空时说明链表中不存在环
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
{
return true;
}
}
return false;
}
那么这里大家一定会有疑惑,为什么 fast 指针一定会追上 slow 指针呢,这其实就是一个追击问题,下面我们画图来解决:
1-> 最开始的时候,slow 与 fast 都指向链表的头结点,并向后移动(fast每次走两步,slow每次走一步)。
2-> 当 slow 走到环的入口时,此时 fast 已经在环内了(可能绕环走了多圈,也可能还未走到一圈),我们设他们之间的距离为 N ,因为他们的相对速度为 1 (fast的速度减去slow的速度),所以之后每走一次,他们之间的距离都会减小 1 ,直到他们同时走了 N 次,fast就追上了slow。
注:在追击的过程中,slow 有没有可能绕环走了一圈,fast 还没有追上呢?答案时不可能的,因为当他们共同走了N次后 ,fast 追上 slow ,而 slow 一次走一步,所以此时 slow 走的距离也就是N,而 N < C (C:环的周长),故此 slow 不会走到一圈,而是在 slow 走第一圈的时候 fast 就会将其追上。
解决思路二(相对速度为 2 时):
当然看到这里,大家肯定有一个疑惑,难道fast 一定要一次走2步吗?不可以一次走3、4、5、......步吗?这里我以 fast 一次走3步,slow 一次走1步为例,来给大家讲解,其他的情况也可以类似分析。
1-> 最开始 fast 与 slow 都指向链表的第一个节点,fast 以一次走 3 步、slow 以一次走 1 步的速度向后走。
2-> 当 slow 走到环的入口的时候,fast 已经在环内了(fast 可能在环内走了多圈),此时他们之间的距离为 N ,之后 slow 就进入环内 ,fast 开始追击 slow ,相对速度为2。
1、当他们之间的距离 N 为偶数时,因为相对速度为2 ,故他们之间的距离变化为 :N 、N-2、N-4、........ 、2、0。所以 fast 在第一次追击过程中就能够追上 slow 。
2、当他们之间的距离 N 为奇数时,因为相对速度为2 ,故他们之间的距离变化为 :N 、N-2、N-4、........ 、3、1、-1。-1代表的就是:当他们之间的距离变成1时,fast 、slow 在同时走一次,fast 将领先 slow 1步,他们之间的距离将变为了 C-1,并且开始新的一轮追击。
(1)、当C为奇数时, C-1 为偶数,在新的一轮追击过程中,他们之间的距离变化为:C-1、C-3、......、4、2、0。fast 将在这一轮能够追上 slow。
(2)、当C为偶数时, C-1 为奇数,他们之间的距离变化为:C-1、C-3、......、3、 1、-1。这表明他们在这一轮的追击过程中,fast 依旧追不上 slow ,他们之间的距离将再次变为C-1,所以 fast 将永远追不上 slow。
但事实并非与我们上述的分析一样,当 N 为奇数,并且 C 为偶数时的这种情况是不存在的,下面我们来进一步分析 N 与 C 之间的关系。
我们将链表的起始节点与环的入口之间的距离设为 L ,设当 slow 走到环的入口时, fast 已经在环内走了 X 圈,故:
slow 走的距离 S1 = L ;①
fast 走的距离 S2 = L + X*C + C-N ; ②
并且 S2 = 3S1;③
将上面的①②③式子联立可得到:
2L = (X+1)*C - N;
故此我们可以得到等式左边的 (X+1)*C - N 一定为偶数:
①、当C为偶数时,(X+1)*C 为偶数,要想(X+1)*C - N 为偶数 ,N 必须为偶数;
②、当C为奇数时,(X+1)*C 为奇数,要想(X+1)*C - N 为偶数 ,N 必须为奇数;
故此,C与N的奇偶性必须相同,所以在上诉我们得出的结论当中,N 为奇数,C为偶数的情况不 存在,所以 fast 一次走3步,slow 一次走1步的解决思路可行,fast 可以追上 slow。
以上也就解决了我们的链表带环问题,如果还想进一步练习,可以刷一下下面的题目。
参考代码:
bool hasCycle(struct ListNode *head) {
struct ListNode * fast = head;
struct ListNode * slow = head;
while(fast && fast->next) //当fast不能再一次走两步就跳出循环
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
{
return true;
}
}
return false;
}
参考代码:
//这里可以画图理解
//设从链表第一个节点到环的入口点之间的长度为L,环的长度为C,
//先让 fast 走C步,再让fast与slow同时走 L 步,相遇的点即为环的入口点
struct ListNode* detectCycle(struct ListNode* head) {
struct ListNode* slow = head;
struct ListNode* fast = head;
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
{
break;
}
}
if (fast == NULL || (fast->next) == NULL) //链表不存在环
{
return NULL;
}
int cnt = 1; //统计环的长度
fast = fast->next;
while (slow != fast) //统计环的长度
{
cnt++;
fast = fast->next;
}
fast = slow = head; //让fast 与 slow 重新指向链表的第一个节点
while (cnt--) //让fast 先走环的长度
{
fast = fast->next;
}
while (fast != slow) //再让fast 与 slow 同时走,他们相遇的点即为环的入口点
{
fast = fast->next;
slow = slow->next;
}
return fast;
}