深度解读面试题:链表中环的入口结点(附代码,可过在线OJ)

在解读“链表中环的入口结点”前,我认为有必要明白关于它的一些用于打基础的问题(相交链表、判断链表中是否存在环)

相交链表

题目:

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。点击此处🤔前往该题
例如:
在这里插入图片描述

为了方便描述这类问题,我们给出更为抽象的逻辑图
在这里插入图片描述

首先我们第一眼能够想到的办法,就是遍历一遍然后一一进行比较。遍历的思路,以上图为例,将A中每个结点逐个和B中的结点进行比较,如果相等,那么该结点便是这两个链表的相交结点。(取部分讲解,A中的c1与B中的b1比较不相等,c1与b2比较不相等, c1与b3比较不相等,c1与c1比较相等!所以c1就是A和B的相交结点)。经过分析,可知这样的算法时间复杂度高达O(n2)。

通常的做法是
(1)获得两个链表的长度之差gap。
(2)长的链表从头开始,先走gap步
(3)长链表从第(2)步的位置开始,短的链表从头开始,一起往后走
(4)第一次遇到相等的结点了,该结点就是两个链表的相交结点
在这里插入图片描述
另外,在统计这两个链表各自的长度,获取差距步数的同时,可以判断这两个链表是否为相交的链表。判断的方式,两个链表的尾部结点(非空结点)相同,那么就是相交的。避免了两个链表根本不相交但依然要去执行上述逻辑,这样的无用功,算是进行了优化。还可以优化的细节,如果其中一个链表为空,那么他们就没有相交结点,直接返回一个空结点即可。代码实现如下:

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    if(headA == NULL || headB == NULL)
    return NULL;

    //1.获取两链表的差距步数
    int lenA = 1, lenB = 1;
    struct ListNode* tailA = headA, *tailB = headB;
    //1.1 获取A链表的长度
    while(tailA->next)
    {
        tailA = tailA->next;
        lenA++;
    }
    //1.2 获取B链表的长度
    while(tailB->next)
    {
        tailB = tailB->next;
        lenB++;
    }
    //如果两个链表根本不相交,就不要做无用功
    if(tailA != tailB)
    {
        return NULL;
    }
    int gap = abs(lenA - lenB);
    struct ListNode* longList = headA;
    struct ListNode* shortList = headB;
    if(lenA < lenB)
    {
        longList = headB;
        shortList = headA;
    }
    //2.长的链表先走差距步
    while(gap--)
    {
        longList = longList->next;
    }

    //3.短的链表从头开始,一起走
    while(longList)
    {
        //遇到相等的结点了,该结点就是相交结点
        if(longList == shortList)
        {
            return longList;
        }
        longList = longList->next;
        shortList = shortList->next;
    }
    return NULL;
}

判断链表中是否存在环

给你一个链表的头节点 head ,判断链表中是否有环。例如:
在这里插入图片描述
点击此处前往该类题, 为了方便描述这类问题,我们给出更为抽象一点的逻辑图
在这里插入图片描述
判断一个链表中存在环,更为普遍的办法是定义两个指针,一个指针一次走一步,另一指针一次走两步,前者我们称之为慢指针,后者称之为快指针。若存在环,那么快指针必定会追上慢指针;若不存在环,快指针必定会先走到链表尾部。

为什么慢指针一次走一步,快指针一次走两步,若存在环,快指针必定会追上慢指针的原理证明:
若存在环,当slow开始进环时,fast已经在环里面了。假设入环前的长度为L,那么当slow开始进环时,fast已经在环内走了L(slow走了L,fast就走了2L,fast入环前已经走了L,故在环内走了L)。下面的这张示意图,是环比入环前距离大的样例。也有可能slow进环前,fast已经在环里走了好几圈了!但要记住无论环大还是环小,fast在环内走了几圈,fast在环内所走的步数一定是L。
在这里插入图片描述
fast去追slow,slow开始进环时,假设它们之间的距离为N,要分清楚这里所指的距离到底是哪一段。
走一次:slow走了一步,fast走了两步。那么slow和fast之间的距离就减少了1,此时距离为N-1
走一次:slow走了一步,fast走了两步。那么slow和fast之间的距离就减少了1,此时距离为N-2
走一次:slow走了一步,fast走了两步。那么slow和fast之间的距离就减少了1,此时距离为N-3
… …
slow在环内走了N次后:此时距离为N-N = 0,fast追上了slow!

在这里插入图片描述
到这里的时候,你可能会产生疑问,为什么一定是慢指针一次走一步,快指针一次走两步。快指针一次走三步行不行?一起验证这样的假设是否可行。

不管fast在环内走了多少,假设slow开始进环时,fast和slow相距N
在这里插入图片描述
走一次:slow走了一步,fast走了三步。那么slow和fast之间的距离就减少了2,此时距离为N-2
走一次:slow走了一步,fast走了三步。那么slow和fast之间的距离就减少了2,此时距离为N-4
走一次:slow走了一步,fast走了三步。那么slow和fast之间的距离就减少了2,此时距离为N-6
… …

如果N是偶数,也就是slow开始进环时,两指针的差距是偶数,距离才会逐渐变为0;否则就有可能会错过

  • 以slow开始进环时,slow和fast距离N为偶数6举例:
    走一次:slow走了一步,fast走了三步。那么slow和fast之间的距离就减少了2,此时距离为4
    走一次:slow走了一步,fast走了三步。那么slow和fast之间的距离就减少了2,此时距离为2
    走一次:slow走了一步,fast走了三步。那么slow和fast之间的距离就减少了2,此时距离为0
    fast追上slow了,该链表存在环!

  • 以slow开始进环时,slow和fast距离N为奇数7举例:
    走一次:slow走了一步,fast走了三步。那么slow和fast之间的距离就减少了2,此时距离为5
    走一次:slow走了一步,fast走了三步。那么slow和fast之间的距离就减少了2,此时距离为3
    走一次:slow走了一步,fast走了三步。那么slow和fast之间的距离就减少了2,此时距离为1
    走一次:slow走了一步,fast走了三步。那么slow和fast之间的距离就减少了2,此时距离为-1
    距离变成了-1?实质上,两者的距离又开始反向变大了!这一次就错过了
    在这里插入图片描述
    当然,还可以让fast继续追slow,这一轮是否会错过,要取决于环的大小,这不就是上一步操作的轮询吗?C为环的大小,此时C-1就是两者之间的距离差N。如果C-1为偶数,那么fast就可以追上slow,从而证明该链表存在环;如果C-1依旧为奇数,那么就会永远错过,无法证明该链表是否存在环。

其他方案请自行证明。 综合而言,慢指针一次走一步、快指针一次走两步是判断链表中是否存在环的最优解决方案。参考代码如下:

bool hasCycle(struct ListNode *head) {
	//如果链表为空,一定不存在环
    if(head == NULL)
    {
        return false;
    }
    struct ListNode* slow = head, *fast = head;
    while(fast && fast->next)
    {
    	//慢指针一次走一步
    	//快指针一次走两步
        slow = slow->next;
        fast = fast->next->next;
        if(fast == slow)
        {
            return true;
        }
    }
    return false;
}

链表中环的入口结点

经过前两个类型的题,算是为这道题打下一些基础知识,
题目:给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

思考“判断链表存在环”类题,并结合其中的两个图,你能联想到什么?
环比入环前长度大的情况:
在这里插入图片描述
入环前的长度、相遇点到入环结点的长度都是L。到这里我们就应该能想到两个链表相交的思想,如果你看不出来,我们可以将其中部分稍微拉平一点。
在这里插入图片描述
再使用“找相交链表的第一个相交结点”的办法,就能够找到找到这个入环结点了。

当然,这是环大小比入环前长度大的情况。环比较小的情况也是一样的,由于采用慢指针一次走一步、快指针一次走两步的策略,慢指针slow开始入环前,快指针fast已经在环内走了L步(不管它究竟在环内走了几圈)。所以环比较小时,fast走过几圈了的长度+快慢指针相遇点到入环结点的长度 = L。
绕过一个弯来说,不论环大还是环小,我们都只需要定义两个指针,一个指针从链表的头部开始走,一个指针从快慢指针相遇点走,都是一次走一步。如果有环,那么它们必定会相遇。相遇的点,便是入环结点
(1)环比较大时,快慢指针相遇点开始走的指针,不会走超过环的一圈。
(2)环比较小时,快慢指针相遇点开始走的指针,会走了环的很多圈。

综上所述,我们可以总结出求解入环结点的思路:
(1)判断链表中是否存在环,同时得到其中快慢指针相遇的结点;
(2)定义两个指针,一个指针从链表头部开始走,另一指针从快慢指针相遇点开始走;
(3)两个指针相遇的结点,就是入环结点。

//判断是否存在环,并且得到快慢指针相遇的结点
struct ListNode* hasCycle(struct ListNode* head){
    if(head == NULL)
    return NULL;

    struct ListNode *slow = head, *fast = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        //fast追上了slow,存在环
        if(slow == fast)
        {
            return fast;
        }
    }
    return NULL;
}
struct ListNode *detectCycle(struct ListNode *head) {
    //判断是否存在环,如果存在则获得快慢指针相遇点
    struct ListNode* meetNode = hasCycle(head);
    if(meetNode == NULL)
    {
        //不存在环
        return NULL;
    }

    //另外定义两个指针
    //一个指针从链表头开始走,一个指针从快慢指针相遇点开始走
    struct ListNode* ptr1 = head, *ptr2 = meetNode;
    while(ptr1 != ptr2)
    {
        ptr1 = ptr1->next;
        ptr2 = ptr2->next;
        //两个指针相遇了,相遇结点便是入环结点
        if(ptr1 == ptr2)
        {
            return ptr1;
        }
    }
    //如果能够到这里,说明第一个结点就是入环结点
    return head;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小小酥诶

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

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

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

打赏作者

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

抵扣说明:

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

余额充值