训练计划 V

标签: 哈希表 链表 双指针

难度: Easy

某教练同时带教两位学员,分别以链表 l1l2 记录了两套核心肌群训练计划,节点值为训练项目编号。两套计划仅有前半部分热身项目不同,后续正式训练项目相同。请设计一个程序找出并返回第一个正式训练项目编号。如果两个链表不存在相交节点,返回 null

如下面的两个链表

在节点 c1 开始相交。

输入说明:

intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0

l1 - 第一个训练计划链表

l2 - 第二个训练计划链表

skip1 - 在 l1 中(从头节点开始)跳到交叉节点的节点数

skip2 - 在 l2 中(从头节点开始)跳到交叉节点的节点数

程序将根据这些输入创建链式数据结构,并将两个头节点 head1head2 传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被视作正确答案 。

示例 1:

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
解释:第一个正式训练项目编号为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

示例 2:

输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Reference of the node with value = 2
解释:第一个正式训练项目编号为 2 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

示例 3:

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:两套计划完全不同,返回 null。从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。

注意:

Submission

运行时间: 136 ms

内存: 29.8 MB

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
        l1 = headA
        l2 = headB
        while l1 != l2:
            l1 = l1.next if l1 else headB
            l2 = l2.next if l2 else headA
        return l1

Explain

此题解通过双指针技巧查找两个链表的相交点。指针 l1 从链表 headA 的头开始遍历,指针 l2 从链表 headB 的头开始遍历。当 l1 到达链表末尾时,将其重定向到链表 headB 的头,同理,当 l2 到达链表末尾时,将其重定向到链表 headA 的头。如果链表有相交点,两个指针最终会在相交点相遇,因为它们走过的路径长度相同(headA + headB = headB + headA)。如果没有相交点,最终两个指针都会同时为 null,循环结束。

时间复杂度: O(M+N)

空间复杂度: O(1)

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
        l1 = headA  # 初始化指针 l1 指向链表 A 的头节点
        l2 = headB  # 初始化指针 l2 指向链表 B 的头节点
        while l1 != l2:  # 当两个指针不相遇时循环
            l1 = l1.next if l1 else headB  # 如果 l1 到达末尾,则指向 headB 的头,否则移至下一个节点
            l2 = l2.next if l2 else headA  # 如果 l2 到达末尾,则指向 headA 的头,否则移至下一个节点
        return l1  # 返回相交节点,如果无交点,则为 null

Explore

这种做法主要是为了消除两个链表长度不同带来的差异,确保两个指针能够在相同长度的路径内遍历完整个结构。当一个指针遍历完自己所在的链表后,转而遍历另一个链表,这样两个指针各自走过的总路径长度为两个链表长度之和。这保证了即使两个链表长度不同,它们也能在遍历完整个路径后在相交点相遇,或者同时到达链表末尾。

每个指针会遍历两个链表的长度之和,即先遍历自己的链表,然后遍历对方的链表。如果两个链表在某个节点相交,则由于每个指针从各自的链表头开始,转而遍历另一个链表,它们将会在相交节点处相遇,因为它们走过的路径长度相同。如果链表不相交,两个指针会同时到达链表的末尾(都为null),因此循环也会在这时结束。

在这种实现中,不会出现死循环。每次迭代中,两个指针都会向前移动一个节点,最终两个指针要么在相交点相遇,要么同时到达两个链表的末尾(null),这两种情况都会导致循环结束。由于每次迭代都会使得至少一个指针向前移动,因此不会有在同一位置无限循环的情况发生。