LeetCode:142 环形链表II

185 阅读2分钟

「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战

题目

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

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

不允许修改 链表

示例 1:

img

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

示例 2:

img

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

示例 3:

img

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

提示:

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

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

解题

解题一:判断重复节点(HashSet)

思路

从头节点开始,依次遍历单链表中的每一个节点,用 HashSet 存放曾经遍历过的 Node 节点,每遍历一个节点判断 HashSet 是否有相同的值,如果发现 HashSet 中存在与之相同的的节点,则说明链表有环,返回 True,如果发现 HashSet 中不存在与之相同的的节点,就把这个新节点存入 HashSet 中,之后进入下一个节点,继续重复刚才的操作。遍历完成后还没有返回值,就返回 -1

代码

/**
 * Definition for singly-linked list.
 */
class ListNode {
    int val;
    ListNode next;

    ListNode(int x) {
        val = x;
        next = null;
    }
}

public class Solution {
    public ListNode detectCycle(ListNode head) {
        // 当链表为空,或者只有一个节点时,不可能构成环形链表,返回 head
        if (head == null || head.next == null) {
            return null;
        }
        // 存放已经遍历过的节点,用于判断是否有相同节点
        HashSet<ListNode> resultSet = new HashSet<>();
        // 遍历链表
        while (head != null) {
            if (resultSet.contains(head)) {
                // 当 resultSet 存在相同节点时,代表形成环形链表,返回 true
                return head;
            }
            // 当resultSet 不存在相同节点时,将遍历的节点存入 resultSet,继续遍历链表
            resultSet.add(head);
            head = head.next;
        }
        // 链表已经遍历完,没有相同节点,代表没有构成环形链表
        return null;
    }
}

总结

特点

  • 使用了 HashSet 作为额外的缓存

注意点

  • 判断是否有相同的节点使用 Node 判断,而不是 Node.val 判断
  • 使用 HashSet 比 HashMap 能更节省空间

性能分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
  • 执行用时:3 ms,在所有 Java 提交中击败了 21.92% 的用户
  • 内存消耗:38.9 MB,在所有 Java 提交中击败了 19.26% 的用户

解题二:双指针法(快慢指针)

思路

将链表有环问题改变为追及问题

  1. 判断链表是存在环
    • 首先创建两个指针 p1 和 p2,让它们同时指向这个链表的头节点。然后开始一个大循环,在循环体中,让指针 p1 每次向后移动 1 个节点,让指针 p2 每次向后移动 2 个节点,然后比较两个指针指向的节点是否相同。如果相同,则可以判断出链表有环,如果不同,则继续下一次循环,直到 p2 指针指向空
  2. 判断链表环的长度
    • 当两个指针首次相遇,证明链表有环的时候,让两个指针从相遇点继续循环前进,并统计前进的循环次数,直到两个指针第 2 次相遇,此时,统计出来的前进次数就是环长,因为指针 p1 每次走 1 步,指针 p2 每次走 2 步,两者的速度差是 1 步。当两个指针再次相遇时,p2 比 p1 多走了整整 1 圈。因此,环长 = 每一速度差 * 前进次数 = 前进次数
    • D:代表从头节点到首次相遇节点的距离
    • S1:代表从入环点到首次相遇点的距离
    • S2:代表从首次相遇点到入环点的距离
    • S1+S2:代表环的长度
  3. 计算从头节点到入环节点的长度
    • 当两个指针首次相遇时,指针 p1 一次只走 1 步,所走的距离时 D+S1,指针 p2 一次走 2 步,多走了 n 整圈,所走的距离是 D+S1+n(S1+S2)
    • 由于 p2 的速度是 p1 的 2 倍,所以所走的距离也是 p1 的 2 倍,因此 2(D+S1) = D+S1+n(S1+S2) -> D=(n-1)(S1+S2)+S2,也就是说,从链表头节点到入环的距离等于从首次相遇点绕环 n-1 圈再回到入环点的距离
    • 把其中一个指针放回到头节点位置,另一个指针保持在首次相遇点,两个指针都是每次向前走 1 步,那么他们最终相遇的节点就是入环点

代码

/**
 * Definition for singly-linked list.
 */
class ListNode {
    int val;
    ListNode next;

    ListNode(int x) {
        val = x;
        next = null;
    }
}

public class Solution {

    public boolean hasCycle(ListNode head) {
        // 当链表为空,或者只有一个节点时,不可能构成环形链表
        if (head == null || head.next == null) {
            return null;
        }
       // p1 指针指向头节点的下一个节点
       ListNode p1 = head.next;
       ListNode p2 = null;
        // p1 指针指向头节点的下一个节点
       if(p1.next !=null){
          // 当 p1 的下一个节点不为 null 时,设置 p2 指针指向 p1 指针的下一个节点
          p2 = p1.next;
       }
       while(p2 !=null && p2.next !=null && p2.next.next !=null){
            // 满足 p1 走 1 步,p2 走 2 步,开始遍历链表
            if (p1 == p2) {
                // 当 p1 == p2 时,代表构成循环链表,将 p1 放回头节点,p1 和 p2 都只走一步,相遇点就是入环点
                p1 = head;
                while (p1 != p2) {
                    p1 = p1.next;
                    p2 = p2.next
                }
                return p1;
            }else {
               // 当 p1 != p2 时,继续循环链表,p1 走 1 步,p2 走两步
               p1 = p1.next;
               p2 = p2.next.next;
            }
        }
        // 链表已经遍历完,没有相同节点,代表没有构成环形链表
        return null;
    }
}

动态图片

总结

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
  • 执行用时:0 ms,在所有 Java 提交中击败了 100% 的用户
  • 内存消耗:38.4 MB,在所有 Java 提交中击败了 73.72% 的用户