【手撕OJ题】——141/142. 环形链表

🕒 题目 Ⅰ

🔎 141. 环形链表【难度:简单🟢】

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

  • 示例 1:
    在这里插入图片描述

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

  • 示例 2:
    在这里插入图片描述

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

  • 示例 3:
    在这里插入图片描述

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

提示:

  • 链表中节点的数目范围是 [0, 104]
  • -105 <= Node.val <= 105
  • pos 为 -1 或者链表中的一个 有效索引 。

进阶:你能用 O(1) 内存解决此问题吗?

⌛ 方法① - 快慢指针

💡 思路:定义两个指针 fastslowslow指针每次移动一步,而fast指针每次移动两步。如果链表带环,二者最终会在链表的某个节点相遇。

代码实现如下:

bool hasCycle(struct ListNode *head) {
    struct ListNode* fast = head, *slow = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if(fast == slow)  //如果快指针与慢指针相遇就代表有环
            return true;
    }
    return false;  //如果循环可以结束就代表没环
}

🕒 面试题

1、为什么快指针每次走两步,慢指针每次走一步,二者最终一定会在环中相遇?

解答:假设slow进环之后,fast和slow之间的差距为N(即追赶距离)。每一次追逐的过程中,距离都会缩小1,过程为:N、N-1、N-2、N-3、…、1、0,距离为0的时候就是相遇

2、快指针一次走三步,慢指针一次走一步,可不可以?

解答:不一定,还是假设进环之后fast和slow之间的差距为N,每追逐一次,之间的距离缩小2步,过程为:N、N-2、N-4、N-6、…,对于这样的结果,如果之间的差距为偶数,最终之间的距离会变为0,说明追上了;如果之间的差距为奇数,最终之间的距离会变成1。然后继续追逐下去,差距就变成了-1,意味着fast和slow之间的距离变成了C-1,C为环的长度,然后重新追逐。
如果C-1是偶数,再追一圈就可以追上,如果C-1是奇数,永远追不上,一直进入新的追逐。

3、快指针一次走X步,慢指针一次走Y步可不可以?
解答:快指针一次走X步,慢指针一次走Y步,慢指针入环时二者的距离为N,快指针与慢指针之间的距离一次缩小X-Y,如果X-Y为1,则一定能够相遇;如果X-Y大于1,如2,3,4… … 可能相遇,也可能不相遇。

🕒 题目 Ⅱ

🔎 142. 环形链表Ⅱ【难度:中等🟡】

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。不允许修改 链表。

示例同上题,输出是返回索引为 pos值 的链表节点。

⌛ 方法① - 快慢指针

💡 思路:定义两个指针 fastslowslow指针每次移动一步,而fast指针每次移动两步。首先求出二者在环中的相遇点;然后一个指针从链表的开始走,另一个指针从相遇点开始走,最终二者会在入环点相遇。

假设环的长度为C,环之前的节点的长度为L,slow与fast在环中X距离处相遇,相遇时快指针已经在环中走了N圈,则如下图所示:

在这里插入图片描述

  • slow走的距离: L + X L+X L+X
  • fast走的距离: L + X + N × C L+X+N×C L+X+N×C
  • fast走的距离是slow的两倍: 2 × ( L + X ) = L + X + N × C 2×(L+X)=L+X+N×C 2×L+X=L+X+N×C
    化简得: L + X = N × C L+X=N×C L+X=N×C
    转化得: L = N × C − X = = > L = ( N − 1 ) × C + C − X L=N×C-X ==>L=(N-1)×C +C-X L=N×CX==>L=N1)×C+CX
  • 所以一个指针从相遇点开始走,另一个指针从头开始走,二者最终会在入环点相遇。

代码实现如下:

struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode* fast = head, *slow = head, *cur = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;

        // 找到相遇的节点
        if(fast == slow)
        {
            struct ListNode* meet = slow;
            // 一个指针从头开始走,另一个指针从相遇点开始走
            while(cur != meet)
            {
                cur = cur->next;
                meet = meet->next;
            }
            return cur;  // 二者最终会在入环点相遇
        }  
    }
    return NULL;  // 无环
}

⌛ 方法② - 转换为链表相交问题

💡 思路:定义两个指针 fastslowslow指针每次移动一步,而fast指针每次移动两步。首先求出二者在环中的相遇点,然后记录下相遇点的下一个节点并让相遇点的next指向NULL,让相遇点的下一个节点作为新链表的,与原链表求交点,最后再把相遇点的next复原。
在这里插入图片描述

注意:

  • 我们需要将相遇点的next置空,否则在求两个链表的交点的时候会发生死循环;
  • 由于题目要求不修改原链表,所以最后我们需要把相遇点的next复原;

🔎 相交链表回顾

代码实现如下:

// 求两个链表的交点
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    // 如果任意一个链表为空,则没有交点
    if (headA == NULL || headB == NULL) {
        return NULL;
    }

    struct ListNode *curA = headA;
    struct ListNode *curB = headB;

    // 当两个指针不同步时,继续循环
    while (curA != curB) {
        // 如果curA到达链表A的末尾,则重置到链表B的头部
        curA = (curA == NULL) ? headB : curA->next;
        // 如果curB到达链表B的末尾,则重置到链表A的头部
        curB = (curB == NULL) ? headA : curB->next;
    }

    // curA和curB要么同时为空(没有交点),要么同时指向交点
    return curA;
}

struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode* slow = head, *fast = head, *cur = head;
    while(fast && fast->next)
    {
        // 迭代
        slow = slow->next;
        fast = fast->next->next;

        // 找到相遇点
        if(slow == fast)
        {
            // 记录相遇点的下一个节点,并把链表从相遇点断开,避免求相交的时候发生死循环
            struct ListNode* meet = fast;
            struct ListNode* next = meet->next;
            meet->next = NULL;

            // 求两个链表相交
            struct ListNode* intersection = getIntersectionNode(cur, next);
            return intersection;

            // 恢复原链表
            meet->next = next;
        }
    }
    return NULL;  // 无环
}

❗ 转载请注明出处
作者:HinsCoder
博客链接:🔎 作者博客主页

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值