【数据结构】环形链表的基本解题方法及证明

        在刷题的过程中,你可能会遇到这样一些题,这些题中的单链表的尾节点的next不是指向NULL,而是指向这个链表的某一个节点,这些题目可能会让你证明这个链表的尾节点的next不为NULL(成环),甚至可能会问你尾节点的next到底指向哪个节点(环的入口)。如果你对这些问题毫无思路的话,希望这篇文章会对你有所帮助。


环形链表的概念

        如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。

        这段简单的文字就是环形链表的概念,光看文字会觉得很抽象,但是我们可以结合图来看。

        上面的链表都为环形链表,即链表的最后一个节点的next不为空,而是指向链表中的任意一个节点,甚至指向自己本身。这样的链表叫做环形链表。

        反之就不算环形链表,如平常我们所遇见、学习的链表。


链表成环的证明方法及其原理

证明方法

        我们先说结论,想要证明链表成环很简单,我们只需要运用快慢指针的方法,定义一个指针fast,一个指针slow,两个指针都从头节点开始运动,fast一次走两步而slow一次走一步,只要链表成环那么两个指针一定会相遇。

bool hasCycle(struct ListNode *head) {
    struct ListNode* slow, *fast;
    slow = head;
    fast = head;

    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;

        if(fast == slow)
        {
            return true;
        }
    }

    return false;
}

        你可能会好奇循环条件为什么是fast && fast->next(fast != NULL || fast->next != NULL),其实这个循环条件是为链表不成环准备的。如果链表不成环,fast一定是最先走到末尾的,所以我们可以以fast的状态为结束条件,那么和fast->next有什么关系呢?那是因为根据链表节点数目的不同,访问链表结束时fast所在的位置是不同的。

        从图中我们可以看见,当链表节点个数为奇数时,在fast访问完链表后,fast的最后位置应为链表的最后一个节点,所以我们需要将fast->next == NULL为循环的结束条件。

 

        当链表节点个数为偶数个时,fast访问完链表时所在的位置为NULL,所以我们应将fast == NULL为结束条件。

        综上所述,循环的条件应为fast && fast->next。

方法原理

        如果你只是需要用来做题,那么记住上面的结论是已经足够了,但是如果去面试,只知道结论是远远不够的,我们还需要知道其中的原理。

        我们先来思考两个问题:

        1、当fast走两步,slow走一步时,为什么slow和fast一定会在环中相遇?会不会在环里面错过,永远遇不上?

        2、为什么slow走一步,fast走两步呢?能不能fast走一次走n步(n > 2)?

        我先来回答一下两个问题:

        1、只要带环,fast和slow在此条件下一定会相遇。

        2、当n > 2时,两者不一定会相遇。

        证明原理其实就是第一个问题的答案,但第二个问题也值得我们思考。我们两个问题都来解决一下。

        第一个问题其实和我们物理中追及相遇问题很像,为什么这么说呢?为了方便,我们不妨设这个链表成环,因为fast一次运动的距离要大于slow,那么fast一定会在slow前面进环,在slow进环前,fast会一直在环中运动(运动的距离与未成环部分的长度有关)。我们不管fast运动了多少,我们假设当slow进环时,fast与slow的距离(fast的运动方向上的距离)为N。

        slow进环后会和fast一起一直绕着环运动,此时追及相遇就开始了。因为运动的速度不同,fast一次走两步,slow一次走一步,所以在每次运动过后,fast一定会比slow多运动一步(fast追及slow),fast和slow间的距离就会减少1,fast和slow之间的距离会随着运动次数的增大而不断缩短,直到相遇。

运动次数fast与slow的相对距离
0N
1N-1
............
N-11
N0(相遇)

 


         所以第一个问题我们就解决完毕了,接下来我们来思考第二个问题。

        我们不妨假设slow一次走一步而fast一次走三步,这样fast和slow间的距离在每次后都会减去二,同样的,我们假设slow进环后,fast和slow间的距离为N,我们来看一下每次运动过后,fast和slow间距离的变化。

fast和slow间的相对距离(N为奇数)fast和slow间的相对距离(N为偶数)
NN
N-2N-2
N-4N-4
N-6N-6
............
12
-1(fast超过了slow)0(相遇)

        我们发现了当N为偶数时,slow与fast会相遇,而当N为奇数时,经过一次追及运动后并不会相遇,我们需要再来看第二次追及运动。

        我们假设环的长度为C,当fast和slow的距离为-1时意味着N = C-1。

         接下来我们再次观察一下fast和slow间距离的变化。

fast和slow间的相对距离(C为奇数,N为偶数)fast和slow间的相对距离(C为偶数,N为奇数)
N(C-1)N(C-1)
N-2N-2
N-4N-4
............
21
0(相遇)-1(fast超过slow)

        我们观察到,当C为奇数时,fast和slow在第二次追及中相遇,而当C为偶数时,fast会再一次超越slow,那么有没有必要进行第三次追及呢?答案是否定的,因为第三次追及的结果与第二次是一样的。

        我们是以fast一次走三步举例,其实不管走四步、五步还是n步都是一样的,不过是把每次相减的距离发生了改变而已。所以我们就得到了答案当n > 2时,两者不一定会相遇,这与环的长度C有关。


如何寻找环的入口

寻找方法

        老样子,我们先说结论:

        一个指针从相遇点开始走,一个指针从链表头开始走,他们会在环的入口点相遇。

struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode* slow = head, *fast = head;

    //验证是否成环
    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;

        //如果相等,说明成环了
        if(fast == slow)
        {
            struct ListNode* meet = slow;
            struct ListNode* curhead = head;

            //查找成环的入口节点
            //如果成环的话,从相遇点开始走和从链表头开始走的指针一定会相遇(以同一速度)
            while(meet != curhead)
            {
                meet = meet->next;
                curhead = curhead->next;
            }

            return meet;
        }
    }

    return NULL;
}

方法原理 

        我们设在相遇时,slow走过的距离为L+X,fast走过的距离为L+X+n*C(n>=1)。

        因为fast的速度为slow的距离的两倍,所以在相同运动时间内,fast运动的总距离为slow运动总距离的2倍。

        故:

        2*(L+X)= L+X+n*C

        L+X = n*C

        L = n*C-X

        L = (n-1)*C + C - X

        因为fast和slow的运动以C为一个周期(从一个点开始走C个长度后,还是该点),所以我们可以直接将C直接省略,即最后的式子为:

        L = C - X

        所以我们就证明出了: 一个指针从相遇点开始走,一个指针从链表头开始走,他们会在环的入口点相遇。


结语及练手题目

        以上就是我总结的环形链表一些基础题目的解题方法和相关证明。希望对你能够有所帮助,如果有错误还请指正。

        经过学习后,我们可以用以下题目练手:

        LeetCode 141.环形链表

        LeetCode 142.环形链表Ⅱ

  • 32
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值