最近遇到了一些关于链表中的环的问题,就好好研究整理了下。环是链表中的尾结点指向前面的某一结点(可以是头结点也可以是中间的结点)。其实用 0 和 6 来形容两种环,很形象,前者尾指向头结点,后者尾指向中间结点。
一般的环相关的问题:
1. 判断是否有环
利用快慢指针来判断。假设p1一次走一步,而p2一次走n(n>1)步。当p2为NULL或者p1和p2指针相同时,停止循环,进行判断,如果指针相等,则存在环。
证明:这里假设p1走的距离为s,那么p2走的为n*s。如果循环结束时p2为NULL了,显然没有环;另一种循环结束情况为p1和p2相等了,也就是两指针相遇了。慢的可以追上快的,说明有回路(环)。
假设环长为r,这里p2走了x圈然后在相遇点和p1相遇,即s+xr = ns => (n-1)s= xr .很多文章都以p2走两步来判断环,其实不是绝对的,这里n>1即p2比p1快即可。而且对于部分文章说的第一次相遇p2比p1快一圈的也不正确,这里就算n=2,s=xr,只有当s =r 也就是在相遇时,恰好p1走的等于环长,才是p2比p1多一圈。n和r,都是视情况而定,取整符合公式即可。但要注意,因为相遇肯定在环里面。
下面为判断环是否存在以及求环入口的代码(来自牛客题我写的通过的):
<span style="font-size:14px;">/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};
*/
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead)
{
ListNode* p1=pHead;
ListNode* p2=pHead;
ListNode* p3=pHead;
if(pHead==NULL||pHead->next==NULL)
return NULL;
do{
p1=p1->next;
p2=p2->next->next;
}while(p2!=NULL&&p2->next!=NULL&&p1!=p2);
if(p1==p2) //到此,p1==p2则存在环
{
//确定存在环之后,返回环的入口点
while(p3!=p1)
{
p3=p3->next;
p1=p1->next;
}
return p1; //这里返回环的入口点
}
return NULL;
}
};
</span>
2.确定存在环之后,求环的入口点
求环入口,为了方便,让n=2(即快的一次两步,慢的一次一步)。用两个指针,p1从相遇点继续出发一次一步,另外一个指针从头出发一次一步,两个指针在环入口相遇。
证明:为了方便可以n=2,但不同的环,r 和s 都是不固定的。于是上面的s+xr = ns => s=xr 又s = L(头到环入口)+Y(环入口到相遇点)所以L+Y = xr => L=xr –Y =(x-1)r+r-Y. 这里不管x 是多少圈,等式左边的L就是p3从头跑的距离;右边p1先跑个r-y的距离,也就是刚好从相遇点跑到了环入口(Y+r-Y=r),再加上(x-1)*r圈,所以p1还是在环入口处,而p1 的距离和p3的相同(速度一样),所以刚好p1和p3相遇点就是入口点。
注意:很多博客说多一圈,或者这里x是1,都是错的。它的原理在于不管多少圈都可以
代码实现在上面代码里加了。
3.求环的长度(不依赖于找到环的入口)
从p1和p2的相遇点开始跑,再次回到p2位置就是环长(因为快慢相遇相遇肯定在环中);或者确定好环的入口点之后,将p3重新从p1跑,回到p1处(p1此时在环入口),也可以记录一圈
<span style="font-size:14px;">p3 = p1->next;
int num=0; //num为环中不包括环入口点的所有点个数
while(p3!=p1){
++num;
p3=p3->next;
}
</span>
4.求链表长
从头跑到入口处p1处,得到L的长度,然后步骤3得到r 的长度,L+r 就是链表长度,和3一样遍历就可以,就不重复写了。