目录
前言
当我们学习单链表的知识点之后,必不可少就是刷关于其它的oj题,那也就一定会遇到环形链表的问题,我们直到,链表是属于线性表,是一对一的数据关系,当某一个节点的next指针指向了此节点或此节点之前的节点,此时就造成了环形链表,在遍历链表时就会死循环,在具体的开发环境中也会出现很大的问题,所以解决环形链表问题就相当的重要。
实际上,解决这个问题所需要写的代码并不多,但是难在解决思路的理解上,而且同时也会产生一些疑问,下面我就带大家来看一下这个问题,并解决大家的疑问。
问题介绍
环形链表问题,顾名思义,就是给你一个链表的头节点,判断此链表中是否存在环,存在返回true,不存在返回false,比如,
解决方法
其实,解决环形链表问题的方法大家并不陌生,写过几个关于链表的oj题的同学都应该知道快慢指针,就是两个指针都指向头节点,一起向后走,快指针一次走两步(一个节点为一个步长),慢指针一次走一步,下面看具体思路。
思路过程:
首先,链表中无环的话,快指针一定会走到最后(即指向NULL),这也是判断链表无环的关键,其次,链表中有环的话,设置快慢指针从头节点出发,主要关注下面几个关键时间点:
①慢指针在后,快指针刚进入环
②慢指针刚进入环,快指针在环中
③此时两个指针都在环中,快指针在前,慢指针在后,快指针追击慢指针 ,一定能追上(后面证明),被追上的标志就是快慢指针相等。
证明(为什么一定能够追上):
从上述思路过程的时间点②说起,假设两个指针相距x个节点(如下图1),之后当慢指针一次走一步,快指针一次走两步,它们的距离为x-1(如下图2),再各自走一次,距离为x-2.......两指针每各自走一次,距离就会减一,距离一定可以减为0,此时两个指针相等,这是判断链表有环的关键。
思考 (如果快指针一次走三步,走四步.......走n步呢):
我们知道慢指针一次走一步,快指针一次走两步,是一定可以判断链表中是否有环的,如果快指针一次走三步,从上面思路过程的关键点②开始,此时快慢指针相距x,之后当慢指针一次走一步,快指针一次走三步,它们的距离为x-2,再各自走一次,距离为x-4.......那我们发现,
1)当起始距离为偶数时,距离最后会变成0(即相等),
2)但当起始距离为奇数时,第一圈时距离不会变成0(...3,1,-1...),
那就会问了 “这一次错过去了,快指针再追一圈时会不会相遇呢,在追一圈呢” ,我们假设环的大小为c,那么当距离减为-1时,此时快慢指针的距离为c-1,
①如果c为奇数,此时c-1为偶数,那么距离最后一定会减为0,
②如果c为偶数,此时c-1为奇数,那么距离最后一定不会减为0,并且之后无论转多少圈快慢指针都不会相遇。
这是快指针一次走三步的情况,那一次走四步,五步......n步类似都可以分析出来。
代码:
struct ListNode {
int val;
struct ListNode *next;
};
typedef struct ListNode Lnode;
bool hasCycle(struct ListNode *head) {
if(!head)
{
return false;
}
Lnode* slow=head;
Lnode* fast=head;
while(fast)
{
slow=slow->next;//慢指针走一步
fast=fast->next;//快指针走两步的第一步
if(!fast)//判断快指针第一步之后有无走到头
{
break;
}
fast=fast->next;//快指针的第二部
if(slow==fast)
{
return true;
}
}
return false;
}
进阶问题介绍
对于上面的初阶问题,只是去判断链表中是否有环,那如果要求我们求出带环链表的入环第一个节点呢?又如何解决,这是环形链表的进阶问题,比如:


解决方法
方法一:
利用相交链表求出环的起始节点。
思路过程:
由初阶问题,可以得出快慢指针相遇的节点,设为meet,meet的下一节点设为new_head,将meet节点的next指针置为空(如图1),此时环就从meet节点处断掉了,那么求环的起始节点的问题就是求原先链表的表头head与new_head的两条链表的相交节点(如图2)。
--->
代码:
struct ListNode {
int val;
struct ListNode *next;
};
typedef struct ListNode Lnode;
//求两个链表的相交节点
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
if((!headA)||(!headB))
{
return NULL;
}
int lena=0;
int lenb=0;
int i=0;
Lnode* pa=headA;
Lnode* pb=headB;
while(pa)
{
lena++;
pa=pa->next;
}
while(pb)
{
lenb++;
pb=pb->next;
}
pa=headA;
pb=headB;
int mid=0;
if(lena>lenb)
{
mid=lena-lenb;
for(i=0;i<mid;i++)
{
pa=pa->next;
}
}
else
{
mid=lenb-lena;
for(i=0;i<mid;i++)
{
pb=pb->next;
}
}
while(pa&&pb)
{
if(pa==pb)
{
return pa;
}
pa=pa->next;
pb=pb->next;
}
return NULL;
}
//求环的起始节点
struct ListNode *detectCycle(struct ListNode *head) {
if(!head)
{
return NULL;
}
Lnode* slow=head;
Lnode* fast=head;
while(fast)
{
slow=slow->next;
fast=fast->next;
if(!fast)
{
break;
}
fast=fast->next;
if(slow==fast)
{
//有环
Lnode* new_head=slow->next;
slow->next=NULL;
return getIntersectionNode(head,new_head); //调用函数
}
}
return NULL;
}
方法二:
根据快指针的步数总是慢指针步数的二倍建立方程得出一定的关系。
思路过程:
将head与环的起始节点的距离设为L,环的起始节点与快慢节点相遇点的距离设为X(如图),我们知道,当慢指针进入环中之后,在进行第二圈之前,快指针一定会追上它(因为快指针的步数总是慢指针步数的两倍,当慢指针开始第二圈时,说明慢指针已经走了完整的一圈,而快指针则是走了完整的两圈,这个过程中它们不可能没有相遇,况且快指针先进的环,所以相遇点一定是慢指针进行第一圈,快指针进行诺干圈的时候出现的),
所以慢指针的总步数为L+X,快指针的总步数为L+N*C+X(C表环的周长,N表圈数),又因为快指针的步数总是慢指针步数的二倍,有等式(L+X)*2=L+N*C+X,化简得L=N*C-X=(N-1)*C+C-X,意思是链表头到环起始节点得距离等于环周长的某倍加上C-X。
因此,在得到相遇点之后,设一个节点指针在meet处,一个节点指针在链表头节点处,同时走,都每次走一步,最后一定会在环起始节点处相遇。
代码:
struct ListNode {
int val;
struct ListNode *next;
};
typedef struct ListNode Lnode;
struct ListNode *detectCycle(struct ListNode *head) {
if(!head)
{
return NULL;
}
Lnode* slow=head;
Lnode* fast=head;
while(fast)
{
slow=slow->next;
fast=fast->next;
if(!fast)
{
break;
}
fast=fast->next;
if(slow==fast)
{
//有环
//让fast从相遇点走,slow从头走,都是一步一步走,再次相遇点就是环的入口
slow=head;
while(fast!=slow)
{
slow=slow->next;
fast=fast->next;
}
return slow;
}
}
return NULL;//无环
}
以上就是环形链表问题的解释了,在力扣中都有对应的oj题可以做,听懂的可以去尝试一下,有不懂或者有疑问的小伙伴可以私我或者评论区,记得三连支持哦!
141. 环形链表 - 力扣(LeetCode)https://leetcode.cn/problems/linked-list-cycle/142. 环形链表 II - 力扣(LeetCode)
https://leetcode.cn/problems/linked-list-cycle-ii/