环形链表详解

一、环形链表的结构和判断方法

1.基础内容

        简单来说,我们可以直观地认为环形链表就是带环的链表,准确地说,就是一个节点的next指向了这个节点之前的一个节点,这样便构成了一个环,如下图:

        在上图中,我们发现如果定义一个指向每个节点的指针cur,cur每次往后走一步(cur=cur->next),那么cur永远不会为空,从第二个节点到最后一个节点会被重复遍历,这时,就发现了一个环形链表,第二个节点到最后一个节点构成了一个环,称第二个节点为入环节点。

        那么,如果给我们一个链表,我们要怎么以代码的形式判断它是不是环形链表呢?(具体题目: 141. 环形链表 - 力扣(Leetcode)

        这里,我介绍一种很常见的思路——快慢指针。具体做法和代码如下:

        1.定义指向head的fast、slow指针

        2.fast每次走步,slow每次走

        3.如果链表带环,则fast与slow会在环内相遇;如果链表不带环,则fast会指向NULL

bool hasCycle(struct ListNode* head)
{
    struct ListNode* fast = head, *slow = head;
    while (fast && fast->next)
    {
        //slow每次走一步,fast每次走两步
        slow = slow->next;
        fast = fast->next->next;
        //当链表带环,fast与slow相遇
        if (fast == slow)
            return true;
    }
    //链表不带环,fast一定会指向空或者为尾节点
    return false;
}

 2.进阶内容

        在第一部分,我们用快慢指针的方式来判断一个链表是否带环,slow指针每次走一步,fast指针每次走两步,那么,我们考虑这样的一个问题:

在链表足够长的情况下,slow每次走一步,fast每次一定要走两步吗,如果大于两步会影响判断结果吗?

      这里我们假设fast指针每次走x步(x>=2),并且考虑链表是环形链表的情况,由于fast走的快,所以当slow走到入环节点时,fast一定已经在环里了(包括入环节点位置),我们记环的大小为C,fast与slow的距离为D(0<=D<C),如图:

        在具体的代码中,我们每次都会判断D,当D为0时,我们就可以判断链表为环状,如果不为0,我们就继续让fast和slow走。

        那么在slow到入环节点后,fast与slow走一次,它们之间的距离就变为D-(x-1)。

        在初阶内容给出的方法中,x=2,代入上式,slow到入环节点并且再走一次后,它们之间的距离变为D-1,如果D-1不为0,两个指针继续走,下一次距离变为D-2……最终,在链表有环的情况下,slow与fast之间的距离一定会由D变为0,也就是slow与fast相遇。

        我们接下来考虑x>2的情况,那么每一次fast和slow距离变化如下:

  D ->   D-(x-1) ->  D-2*(x-1)  ->  ……  ->  D - n*(x-1) ,其中n为slow入环后走的次数

        在有环的情况下,如果fast每次走大于两步改方法也可以正确判断链表是否有环,那么最终距离也一定会变为kC(即fast == slow),其中k为不大于0的整数,C为环的长度,也就要有一个整数n可以满足下式成立:

D - n*(x-1) = kC

        由整数的性质,我们得到这个关于n的方程是否有整数解只与C%(x-1) 有关,与D无关。如果这一步不理解可以像之前x=2那样取x=3为例,自己写出距离的变化过程。这里给出具体结论:

当 C % (x-1)  == 0 时 , 方程才有整数解,否则没有整数解。

当x>2时,方程是否有解与环的大小C有关,如果fast每次走的步数大于2,则无法判断链表是否带环。

当x=2时,方程一定有整数解 n = D,因此fast每次走两步可以判断链表是否带环。

二、寻找环形链表的入环节点

        在掌握了如何判断一个链表是否带环后,我们深入研究另外一个问题,如何寻找环形链表的入环节点?(具体题目:142. 环形链表 II - 力扣(Leetcode)

         首先,我们给出一个结论(具体证明在后面):

一个指针从相遇点开始走,一个指针从链表头开始走,则这两个指针一定会在入环节点处相遇。

        通过这个结论我们只要找到相遇点(meetNode),然后一个meetNode指针指向相遇点,另一个head指针指向头节点,然后不断迭代,直到二者相遇,那么相遇位置即为入环节点。

        在寻找相遇点这一步我们可以复用第一问判断链表是否带环的代码,将return true改为meetNode = fast,将return false改为return NULL。具体代码如下:

struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode *fast=head, *slow=head;
    while(fast && fast->next)
    {
        fast=fast->next->next;
        slow = slow->next;
        if(fast == slow)
        {
            //找相遇点meetNode
            struct ListNode* meetNode = fast;
            //相遇点可能就是入环节点
            if(meetNode == head)
                return head;
            //meetNode和head开始每次走一步,直到相遇
            while(head && meetNode)
            {
                meetNode = meetNode->next;
                head = head->next;
                //当相遇时即为入环节点
                if(meetNode == head)
                   return meetNode;
            }
        }
    }
    return NULL;
}

        那么我们要怎么证明上面的结论呢?其实思路和第一个问题的进阶内容想法差不多。

        假设链表带环,头节点head与入环节点的距离为L,入环节点与相遇点的距离为D,环的大小为C,如下图:

         我们考察在方法一判断链表是否带环中让fast和slow指针从链表头节点开始走直到相遇的这个过程,当fast指针和slow指针在meetNode相遇时,分别计算fast和slow所走过的节点数:

fast从头节点到相遇点:L + D + kC,其中k为正整数,表示在快慢指针相遇前fast所走圈数

slow从头节点到相遇点:L + D

        又由于fast每次走两步,slow每次走一步,以上二式可以建立起联系:

L + D + kC = 2 * (L + D)

        整理后可得:

L = kC - D = (k - 1) * C + C - D     

        我们研究化简后的式子,L可以表示从头节点开始走到入环节点的距离,C - D可以表示从相遇点开始走到入环节点的距离,而(k-1)* C不影响最终在环上的位置,因此,可以让一个指针从head开始走,另一个指针从meetNode开始走,它们在链表有环的情况下一定会在入环节点相遇。于是结论得证!

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ChenxuanRao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值