如何判断单链表是否有环、环的入口、环的长度和总长

问题描述
1.如何判断单链表是否有环?
2.如果有环,求出环的入口
3.求环长
4.求总长

注意这里长度:节点的数量
链表定义参考:http://blog.csdn.net/dawn_after_dark/article/details/73610674

探讨
要想判断有环,我们可以联系实际生活中的例子,很容易就想到操场上跑圈,因为是环形,所以快的肯定会追上慢的,所以我们可以应用到链表上,用一个快指针和一个慢指针,但是细想一下发现,我们在跑操的时候相遇时坐标位置不一定是整数啊(这里相比链表节点而言的),而链表是一个节点连接起来,我们怎么做,能让他们在节点上相遇呢,这里就要为2个指针找到合适的速度,使之能够恰巧在某一结点上相遇。

原理:如果快的指针走到NULL,说明无环;而fast==slow相遇,则证明肯定存在环。

公式推导
为什么存在环的情况下,两个指针会相遇呢?以下推到n都是指 环长!

问题一
这里写图片描述
1.假定2个指针同一个起点
我们让两个指针全部指向头节点,然后给slow指针的速度为一步,而fast指针的速度为M步,则在第i次迭代的时候,slow指针走到i mod n,而fast指向Mi mod n,要想slow和fast相遇,则i mod n=Mi mod n,(M-1)i mod n,则我们可以令M=2(最小的可以取得值),i mod n = 0,则 i=n时,相遇,所以我们可以给fast 2倍的速度,这样它们会在 最后一个节点相遇。
2.假定不在同一个起点,并且fast提前K位置
其实这个类似链表中含有个小环的情况,即不是所有点在环中的情况,这样当slow即将进入环状的时候,fast已经在环中k mod n位置了,所以问题转化为假定不在同一个起点,并且fast提前K位置,是否会在一点相遇?
这里写图片描述
fast的速度仍设置为2倍,假定第i次迭代时,slow指向i mod n,fast指向k+2i mod n,其k大于0小于你,那么i ≡ (2i+k)(mod n) -> (i+k) mod n = 0 -> 当i=n-k时,p与q相遇。
这里写图片描述
变相理解,如何同一个起点出发,他们会在整圈(也就是最后一个节点)相遇,现在fast在提前K位置出发,这样就会使相遇点本来是最后节点,现在fast少走k步,即可与slow相遇,所以在n-K位置相遇。类似问题求倒数第K个节点:http://blog.csdn.net/dawn_after_dark/article/details/73611115
所以不管是图1的链表,还是图2的链表,只要有环,快指针跟慢指针相遇,逆命题也成立;所有当快指针跟慢指针相遇,就一定存在环。
代码:

bool LinkList::isContainCirle() {
Node* slow = head;
Node* fast = head; //都指向头节点
while (fast&&fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
break;
}
}
return !(fast==NULL || fast->next==NULL); //只要有一个为空,就说明无环
}

问题二
方法一
我们已经在上面的讨论中,已经得知slow与fast会在环中n-k位置相遇,我们先靠主观方面来探讨这个问题,两个指针同时从头节点开始走,当慢指针即将进入环中的时候,快指针位于k mod n,说明慢指针走的这段路程也能对应k mod n, 因为快指针是慢指针速度的2倍,所以快指针在环中走的距离与慢指针走的距离一样。而我们发现相遇点位于n-k,再走k步就可以到达环的入口,并且慢指针走的路程也能对应k mod n,所以我们再令取2指针,一个指向头节点,另一个指向碰撞点,都以1步的速度前进,这两个指针相遇点就是环的入口,这个结论适用于全环的链表,因为这时k=0,头节点走一步就到了环的入口了。
以上只是我们主观的理解方式,如果采用推导呢,slow走过的路程为s,环长为n,所以,2s=s+k+(m-1)n,化简为s=k+(m-1)n,所以slow在环外相当于走了k+(m-1)n。
而碰撞点位于n-k的位置,所以要想走到环入点,则需要走k+mn步,这时你就会发现只要让这两个指针从头节点与碰撞点以一步的速度过来,就一定会在环入点相遇,从而求出环入点!
代码:

Node* LinkList::findEnterCircle() {
Node* slow = head;
Node* fast = head;
Node* enterCircle;
while (fast&&fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
break;
}
}
if (fast == NULL || fast->next == NULL) { //如果没有环,返回0
enterCircle = NULL;
}
else {
for (enterCircle = head;enterCircle != slow;enterCircle = enterCircle->next, slow = slow->next); //头节点、相遇点同时起步,适用于全环情况,可以自行检验
return enterCircle;
}
}

方法二:
利用方法三求出的环长做,思路很简单,思路就是既然我已经知道环长了,我完全可以使2个指针同时指向头节点,然后令一个指针走一个环长的距离,再让另一个指针开始走,这样两个指针距离始终隔一个环长,相遇的时候,就是环入口点。
思路来自这篇博客:http://blog.csdn.net/elicococoo/article/details/51173166
代码:

Node* LinkList::findEnterCircle() {
Node* enterCircle = head,*q=head;
int length = getCircleLength();
for (int i = 0;i < length;i++) {
q = q->next;
}
while (enterCircle != q) {
enterCircle = enterCircle->next;
q = q->next;
}
return enterCircle;
}

扫描二维码关注公众号,回复: 2759239 查看本文章

方法三
该方法需要先确定是否有环,否则结果无效。思路是用2个指针,其中一个是另一个的前驱,每次都把前驱的next指向NULL,即断开,然后把后继的指针赋值给前驱,后继指针继续后移,这样当后继指针为空时,前驱指针指的就是环入口点。因为环的入后的点在第一次进入环的时候断开了,所以再次循环到这的时候,指针停住的地方就是环的入口。

此方法破坏了原有链表的结构,不提倡这样做,但是思路很好,我们在链表定义的时候加入辅助标记变量,达到伪断开的目的。

问题三
求环长就比较简单了,保存相遇点,然后令一指针从这出发向前,记录走过的节点数,直到指针与相遇点相遇,即可求出环长。
代码:

int LinkList::getCircleLength() { //环长,在链表中一般指的是链表长度
Node* slow = head;
Node* fast = head;
int circleLength;
while (fast&&fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
break;
}
}
if (fast == NULL || fast->next == NULL) { //如果没有环,返回0
circleLength = 0;
}
else{
circleLength = 1; //如果有环,首先肯定包括尾节点
for (Node* p = slow->next;slow != p;p = p->next) { //每走一步加一
circleLength++;
}
}
return circleLength;
}

问题四
总长度等于环外长度+环长度,环长度我们已求出,求环外长度,只需一个指针从头节点开始到环入口结束就行,记录走过的节点数目。
代码:

int LinkList::getCircleLinklistLength() {
int totalLength;
totalLength = getCircleLength();
Node* enterCircle = findEnterCircle();
if (enterCircle) {
for (Node* p = head;p->next != enterCircle;p = p->next) {
totalLength++;
}
}
return totalLength;
}

猜你喜欢

转载自blog.csdn.net/sinat_37529004/article/details/81632651