环形链表判断和寻找入环点

本文分享一道非常经典的题目,选自力扣。
题1:link
题2:link
共两道OJ,我们一道道的看。

一.判断是否为环形链表

原题如下
环形链表判断和寻找入环点_第1张图片
大家可千万别被题目那个pos整蒙了,其实我们解题时,无需关注那个pos。
我们的任务是判断题目所给链表是否带环。

1.题目分析

乍一看题,似乎感觉简单。如果链表不带环,链表将以NULL结束,我们似乎只需遍历链表,看是否能找到NULL。但仔细想想这种方法并不可行,因为如果链表带环,你的程序就会一直遍历链表,你也不知道这到底是链表过长还是链表带环。

2.解法

再一想,相信很多朋友都会想出正确解法——快慢指针。
起始时快慢指针指向链表头,然后慢指针每次走一步,快指针每次走两步,如果链表带环,那么当慢指针进入环时,快指针就会追逐慢指针,因为快慢指针每次所走相距1步,所以每次移动时快慢指针之间的距离就会减一,这种情况下,他们总会相遇,证明链表带环。
如果链表不带环,那么当快指针指向NULL时结束循环即可。
上代码

bool hasCycle(struct ListNode *head)
{
    struct ListNode* slow = head, *fast = head; //快慢指针

    //这里不可直接写fast, 否则后面fast一次移动两步时可能造成NULL解引用
    while (fast && fast->next)
    {
        slow = slow->next; //走一步
        fast = fast->next->next; //走两步

        if (slow == fast)
        {
            return true;
        }
    }
    return false; //fast 或 fast->next 为NULL
}

代码较为简单,不再赘叙。

3.精华

如果你在面试时遇到这道题,那可不仅仅是这么简单。
面试官会问你,你如何证明这样快慢指针就一定会相遇?如果slow一次走1步fast一次走3步,情况如何?fast一次走4步?走5步?……
分析:
下面只讨论带环的情况。
我们将链表抽象为下图,初始时快慢指针指向链表头。
环形链表判断和寻找入环点_第2张图片

无论slow和fast一次所走的步数,当fast进入环即到达P点时,slow还未进环,当slow刚刚到达P点时,fast已经进入环。
此时,我们假设fast和slow之间的距离为N,环的长度为C
环形链表判断和寻找入环点_第3张图片
此时,如果快慢指针每次的步数相差1,如slow走1步,fast走2步,必能追上,因为每走一次,快慢指针之间的距离N就减少1,当N减为0时,快慢指针相遇。

但是,如果快慢指针每次的步数相差大于1,如slow走1步,fast走3步,此时每走一次,快慢指针之间的距离N就减少2。

如果N是偶数,那么N也可减为0,快慢指针相遇。

但N若为奇数,N将被减为-1,意味着快指针将会越过慢指针一步,此时的N就变成了C-1,以后每一步N同样是一次减2,这里同样有两种情况。

C为奇数,C-1位偶数,N可以减到0,快慢指针可以相遇。

C为偶数,C-1为奇数,N只能减到-1,此时快又再一次越过慢指针一位,即N又再次等于N-1,程序死循环。

即当快慢指针每次所走步数相差2时,若N为奇数,C为偶数,则程序死循环。
错误案例:
环形链表判断和寻找入环点_第4张图片
大家可以自己走一下试试。
同理,当快慢指针每次所走步数相差3步时,N不是3的倍数,且C-1或C-2也不是3的倍数,程序死循环……

二.寻找环形链表的入环位置

原题如下:
环形链表判断和寻找入环点_第5张图片
同样解题无需关注那个pos。

1.题目分析

这道题是上一道题的进阶版本,上一道题是要求我们判断链表是否带环,这一道题是让我们寻找链表环的入口点,并返回该节点,如果链表不带环,返回NULL。

2.解法

同样的,我们重点讨论带环的情况。
这道题的解法也相当于上一道题的进阶,同样是使用快慢指针,现在我们直接上结论。
结论:同样的,我们让慢指针一次走一步,快指针一次走两步,当快慢指针在环中相遇时,记录下这个相遇点M。然后我们定义指针cur从链表头开始走,让刚才在相遇点的slow指针也开始走,他们每次都走一步,他们的相遇点即为环的入口点。
上代码:

struct ListNode *detectCycle(struct ListNode *head)
{
    struct ListNode* slow = head, *fast = head;//快慢指针
    struct ListNode* cur = head;

    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)
        {
            break;  //此时slow指针在相遇点处
        }
    }
    if(!fast || !(fast->next))  //链表不带环情况
    {
        return NULL;
    }
    while(slow != cur)  //slow和cur开始走,相遇结束
    {
        slow = slow->next;
        cur = cur->next;
    }
    return cur;//相遇点
}

同样的,代码相对简单,不再赘述。

3.证明

下面我们来证明上面的结论,这才是这道题的精华所在。
我们讨论的都是慢指针一次走一步,快指针一次走两步的情况。
环形链表判断和寻找入环点_第6张图片

如上图,当快慢指针在环中相遇时,我们假设几个变量。
L:链表非环部分的长度,即链表头到入环点之间的距离。
C:链表所带环的长度。
X:入环点到快慢指针相遇点的距离。
这里,我们来思考几个问题。
首先,快慢指针相遇时,慢指针移动的距离是多少?
很容易想到,因为快指针一次比慢指针多走一步,因此慢指针进入环时,在一圈之内,快指针一定就能追上慢指针。因此慢指针所移动的距离为 L + X
同样的,我们思考快指针移动的总距离。
这里,我们可以想到,在慢指针进入环之前,快指针就已经进环,如果环的长度较短,那么在慢指针进环之前,快指针有可能已经绕环转了很多圈。我们假设慢指针进入环时,快指针转了n圈,那么当快慢指针相遇时,快指针移动了 L + nC + X,这里的n大于等于1。
以为快指针移动的距离是慢指针的两倍,所以我们可以得到:
2 * (L + X) = L + nC + X
化简得:
L + X = nC = (n-1)C + C
即:
L = (n-1)C + C-X
从这个表达式中,我们就可以看出上面结论的正确性。
首先,C - X 表示的就是快慢相遇点到入环点之间的距离,它加上(n-1)被的环的长度,就等于链表头到入环点之间的距离。
那么我们就可以将一个指针cur放在头结点,一个指针miss放在环中快慢指针相遇点。他们同时出发,每次走一步,总会在入环点相遇。如果此时环的长度很短,那么n就会很大,也就是说,miss会在环中转n-1圈后再走C-X的距离与cur在入环点相遇。

三.总结

这两道题主要考察的是大家的逻辑思维能力,有些偏向于数学,代码都相对较为简单。所以大家在做这两道题时,可不能仅仅AC就完事。一定要细细推敲其中的逻辑关系,做到真正掌握。

你可能感兴趣的:(数据结构,链表,leetcode)