美文网首页
算法 1.5.1 环形链表 II【leetcode 142】

算法 1.5.1 环形链表 II【leetcode 142】

作者: 珺王不早朝 | 来源:发表于2021-01-05 23:11 被阅读0次

题目描述

给定一个链表,返回链表开始入环的第一个节点
如果链表无环,则返回 null

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)
如果 pos 是 -1,则在该链表中没有环
注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中

说明:不允许修改给定的链表

进阶:你是否可以使用 O(1) 空间解决此题?

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

示例 2:
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

提示:

  • 链表中节点的数目范围在范围 [0, 10^4] 内
  • -10^5<= Node.val <= 10^5
  • pos 的值为 -1 或者链表中的一个有效索引

数据结构

  • 链表(题目自带)、指针变量

算法思维

  • 遍历、快慢指针、逆推、数学思维:环的同步

解题要点

  • 环长度的计算
  • 数学思维的运用:环的同步
    相同速度下,领先一个环长的指针将和从 head 出发的指针在环的首节点相遇,并从此进入同步

解题思路

一. Comprehend 理解题意
  • 需要判断链表中是否有环
  • 需要找到环的第一个节点
  • 不能破坏或改变环的结构和数据

可以使用“追击问题”的思路来判断环的存在,但如何找到环的第一个节点呢?

首先想到的肯定是给链表加一个 int 类型的属性,相当于索引或者编号,但题目明确要求了“不能破坏或改变环的结构和数据”,所以这种方法肯定是不行的

题意是希望我们使用遍历的方式,不改变链表,光靠“看”和“算”,找到环的首节点,而一旦涉及到“算”,我们就必须使用数学方法来解决问题

二. Choose 选择数据结构与算法
解法一:一快两慢指针法
  • 数据结构:指针变量
  • 算法思维:遍历、快慢指针、逆推、数学思维:环的同步
(一) 环首节点的定位

要定位到链表中的某个节点,需要特定的判断条件
要创造判断条件,我想到如下的两个方式:

  1. 索引
    虽然题目中不允许给链表添加属性,但是我们可以使用一个 int 类型的变量 n,实时记录当前遍历的位置。
    如果我们通过某个数学公式,计算出了环的首节点在链表中位置,就可以通过索引来控制遍历的步数,从而使用条件 i == n 进行定位:
ListNode li = head;
for (int i=0; i<n i++) {
    li = li.next;
}
return li;

很可惜,经过各种尝试,仍然没能找到首节点位置与相遇位置之间的关系,即无法通过快慢指针相遇的位置计算出首节点索引,只能再想其他办法

  1. 相遇
    如果有两个指针 a 和 b 恰好在环的首节点相遇,这时就可以使用条件 a == b 进行定位:
while (a != b) {...}
return a;

那么,该如何控制两个指针,让它们恰好在环的首节点相遇呢?

  • 环形链表 I 题中可知,快慢指针并不一定会相遇在环的首节点,因此使用快慢指针肯定是不行的,故此考虑使用两个速度均为 1个步长 的慢指针,方便控制

这里可以使用 反推 / 逆推 的方法,去“凑”相遇条件。

想象一个环形的跑道(如上图),两个小人若要在起点相遇,则需要满足两个条件:

  1. 两个小人速度相同
  2. 黄色小人刚好领先蓝色小人一圈的距离

于是得出结论:相同速度下,领先一个环长的指针将和从 head 出发的指针在环的首节点相遇,并从此进入同步

(二) 环长的计算

明晰了相遇条件(步长相等 + 领先一个环长),接下来就是如何计算环的长度了,环长的计算比较容易:

  1. 定义快慢两个指针:
    slow:从 head 开始,逐个节点遍历链表(步长为 1)
    fast:从 head.next 开始,跨一个节点遍历链表(步长为 2)
  2. slow 和 fast 相遇时走过的 “步数n” 是相同的,因此 fast 走过的距离是 2n,slow 走过的距离是 n
  3. 由相遇条件,二者相遇时 fast 比 slow 多走了一圈,即有:
    2n + 1 = n + 环长 --> 环长 = 2n-n+1 = n+1

至此,所有的准备工作都已经完毕,可以开始实现思路了!!

三. Code 编码实现基本解法
实现步骤:
  1. 声明一个用来记录步数的变量
  2. 声明快慢两个指针
  3. 以不同的步长遍历链表
  4. 先判断是否有环
  5. 如果有环,计算环的长度 = n+1
  6. 声明一个新的慢指针,从 head 开始遍历
  7. slow 前进一步,此时 slow 领先了 slow2 一个 cycLen
  8. 以一倍步长遍历链表,直到 slow 和 slow2 相遇
  9. 相遇的位置即为环的第一个节点
代码如下:
public class Solution {
    public ListNode detectCycle(ListNode head) {

        //0.非空判断
        if (head == null) return null;

        //1.声明一个用来记录步数的变量
        int n = 0;

        //2.声明快慢两个指针
        ListNode slow = head;
        ListNode fast = head.next;

        //3.以不同的步长遍历链表
        while (fast != null && fast.next != null) {
            //4.先判断是否有环
            if (slow == fast) {
                //5.如果有环,计算环的长度 = 1 + fast 路程 - slow 路程 = 1 + 2n - n = n+1
                int cycLen = n+1; //这一步只是方便理解,可以省略
                //6.声明一个新的慢指针,从 head 开始遍历
                ListNode slow2 = head;
                //7.slow 前进一步,此时 slow 领先了 slow2 一个 cycLen
                slow = slow.next;
                //8.以一倍步长遍历链表,直到 slow 和 slow2 相遇
                while (slow != slow2) {
                    slow = slow.next;
                    slow2 = slow2.next;
                }
                //9.相遇的位置即为环的第一个节点
                return slow;
            }
            slow = slow.next;
            fast = fast.next.next;
            n++;
        }

        return null;
    }
}

执行耗时:0 ms,击败了 100.00% 的Java用户
内存消耗:38.2 MB,击败了 97.56% 的Java用户
时间复杂度:O(n) -- 链表的遍历 O(n)
空间复杂度:O(1) -- 3个指针变量的内存空间 O(n)

四. Consider 思考更优解

=== 暂无 ===

五. Code 编码实现最优解

=== 暂无 ===

六. Change 变形与延伸

=== 待续 ===

相关文章

网友评论

      本文标题:算法 1.5.1 环形链表 II【leetcode 142】

      本文链接:https://www.haomeiwen.com/subject/sezuoktx.html