哈希表题目:相交链表

题目

标题和出处

标题:相交链表

出处:160. 相交链表

难度

2 级

题目描述

要求

给你两个单链表的头结点 headA \texttt{headA} headA headB \texttt{headB} headB,请你找出并返回两个单链表相交的起始结点。如果两个链表不存在相交结点,返回 null \texttt{null} null

图示两个链表在结点 c1 \texttt{c1} c1 开始相交:

示例 0

题目数据保证整个链式结构中不存在环。

注意,函数返回结果后,链表必须保持其原始结构

自定义评测:

评测系统的输入如下(你设计的程序不适用此输入):

  • intersectVal \texttt{intersectVal} intersectVal——相交的起始结点的值。如果不存在相交结点,这一值为 0 \texttt{0} 0
  • listA \texttt{listA} listA——第一个链表。
  • listB \texttt{listB} listB——第二个链表。
  • skipA \texttt{skipA} skipA——在 listA \texttt{listA} listA 中(从头结点开始)跳到交叉结点的结点数。
  • skipB \texttt{skipB} skipB——在 listB \texttt{listB} listB 中(从头结点开始)跳到交叉结点的结点数。

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

示例

示例 1:

示例 1

输入: intersectVal   =   8,   listA   =   [4,1,8,4,5],   listB   =   [5,6,1,8,4,5],   skipA   =   2,   skipB   =   3 \texttt{intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3} intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出: Intersected   at   ‘8’ \texttt{Intersected at `8'} Intersected at ‘8’
解释:相交结点的值为 8 \texttt{8} 8(注意,如果两个链表相交则不能为 0 \texttt{0} 0)。
从各自的表头开始算起,链表 A \texttt{A} A [4,1,8,4,5] \texttt{[4,1,8,4,5]} [4,1,8,4,5],链表 B \texttt{B} B [5,6,1,8,4,5] \texttt{[5,6,1,8,4,5]} [5,6,1,8,4,5]
A \texttt{A} A 中,相交结点前有 2 \texttt{2} 2 个结点;在 B \texttt{B} B 中,相交结点前有 3 \texttt{3} 3 个结点。

示例 2:

示例 2

输入: intersectVal   =   2,   listA   =   [1,9,1,2,4],   listB   =   [3,2,4],   skipA   =   3,   skipB   =   1 \texttt{intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1} intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出: Intersected   at   ‘2’ \texttt{Intersected at `2'} Intersected at ‘2’
解释:相交结点的值为 2 \texttt{2} 2(注意,如果两个链表相交则不能为 0 \texttt{0} 0)。
从各自的表头开始算起,链表 A \texttt{A} A [1,9,1,2,4] \texttt{[1,9,1,2,4]} [1,9,1,2,4],链表 B \texttt{B} B [3,2,4] \texttt{[3,2,4]} [3,2,4]
A \texttt{A} A 中,相交结点前有 3 \texttt{3} 3 个结点;在 B \texttt{B} B 中,相交结点前有 1 \texttt{1} 1 个结点。

示例 3:

示例 3

输入: intersectVal   =   0,   listA   =   [2,6,4],   listB   =   [1,5],   skipA   =   3,   skipB   =   2 \texttt{intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2} intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出: No   intersection \texttt{No intersection} No intersection
解释:从各自的表头开始算起,链表 A \texttt{A} A [2,6,4] \texttt{[2,6,4]} [2,6,4],链表 B \texttt{B} B [1,5] \texttt{[1,5]} [1,5]
由于这两个链表不相交,所以 intersectVal \texttt{intersectVal} intersectVal 必须为 0 \texttt{0} 0,而 skipA \texttt{skipA} skipA skipB \texttt{skipB} skipB 可以是任意值。
这两个链表不相交,因此返回 null \texttt{null} null

数据范围

  • listA \texttt{listA} listA 中结点数目为 m \texttt{m} m
  • listB \texttt{listB} listB 中结点数目为 n \texttt{n} n
  • 1 ≤ m,   n ≤ 3 × 10 4 \texttt{1} \le \texttt{m, n} \le \texttt{3} \times \texttt{10}^\texttt{4} 1m, n3×104
  • 1 ≤ Node.val ≤ 10 5 \texttt{1} \le \texttt{Node.val} \le \texttt{10}^\texttt{5} 1Node.val105
  • 0 ≤ skipA ≤ m \texttt{0} \le \texttt{skipA} \le \texttt{m} 0skipAm
  • 0 ≤ skipB ≤ n \texttt{0} \le \texttt{skipB} \le \texttt{n} 0skipBn
  • 如果 listA \texttt{listA} listA listB \texttt{listB} listB 没有交点, intersectVal \texttt{intersectVal} intersectVal 0 \texttt{0} 0
  • 如果 listA \texttt{listA} listA listB \texttt{listB} listB 有交点, intersectVal = listA[skipA] = listB[skipB] \texttt{intersectVal} = \texttt{listA[skipA]} = \texttt{listB[skipB]} intersectVal=listA[skipA]=listB[skipB]

进阶

你能否设计一个时间复杂度 O(m   +   n) \texttt{O(m + n)} O(m + n)、空间复杂度 O(1) \texttt{O(1)} O(1) 的解法?

解法一

思路和算法

如果两个链表相交,则一定存在一个相交的起始结点,从链表相交的起始结点到链表末尾结点的全部结点是两个链表共用的结点。遍历两个链表各一次,则两个链表共用的结点会被重复访问,第一个被重复访问的结点即为链表相交的起始结点。

如果两个链表不相交,则不存在两个链表共用的结点。遍历两个链表各一次,任何结点都不会被重复访问。

因此可以遍历两个链表各一次,根据是否有结点被重复访问判断两个链表是否相交。为了判断是否有结点被重复访问,可以使用哈希集合存储访问过的结点。

首先遍历链表 listA \textit{listA} listA,将每个结点加入哈希集合,然后遍历链表 listB \textit{listB} listB,对于链表 listB \textit{listB} listB 中遍历到的每个结点判断是否在哈希集合中。在遍历链表 listB \textit{listB} listB 的过程中遇到的第一个在哈希集合中的结点即为链表相交的起始结点,返回该结点。如果遍历链表 listB \textit{listB} listB 结束之后仍没有遇到在哈希集合中的结点,则两个链表不相交,返回 null \text{null} null

代码

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        Set<ListNode> visited = new HashSet<ListNode>();
        ListNode temp = headA;
        while (temp != null) {
            visited.add(temp);
            temp = temp.next;
        }
        temp = headB;
        while (temp != null) {
            if (visited.contains(temp)) {
                return temp;
            }
            temp = temp.next;
        }
        return null;
    }
}

复杂度分析

  • 时间复杂度: O ( m + n ) O(m + n) O(m+n),其中 m m m n n n 分别是链表 listA \textit{listA} listA listB \textit{listB} listB 的结点数。需要遍历两个链表各一次。

  • 空间复杂度: O ( m ) O(m) O(m),其中 m m m 是链表 listA \textit{listA} listA 的长度。需要使用哈希集合存储链表 listA \textit{listA} listA 中的全部结点。

解法二

思路和算法

解法一使用哈希表,空间复杂度是 O ( m ) O(m) O(m)。使用双指针可以将空间复杂度降低到 O ( 1 ) O(1) O(1)

创建两个指针 pointer 1 \textit{pointer}_1 pointer1 pointer 2 \textit{pointer}_2 pointer2,指针 pointer 1 \textit{pointer}_1 pointer1 依次遍历链表 listA \textit{listA} listA 和链表 listB \textit{listB} listB 的每个结点,指针 pointer 2 \textit{pointer}_2 pointer2 依次遍历链表 listB \textit{listB} listB 和链表 listA \textit{listA} listA 的每个结点,初始时分别指向两个链表的头结点 headA \textit{headA} headA headB \textit{headB} headB

指针 pointer 1 \textit{pointer}_1 pointer1 的遍历过程是:从 headA \textit{headA} headA 开始依次遍历链表 listA \textit{listA} listA 的每个结点,当遍历完链表 listA \textit{listA} listA 之后, pointer 1 \textit{pointer}_1 pointer1 指向 null \text{null} null,下一步 pointer 1 \textit{pointer}_1 pointer1 指向 headB \textit{headB} headB,然后依次遍历链表 listB \textit{listB} listB 的每个结点,当遍历完链表 listB \textit{listB} listB 之后, pointer 1 \textit{pointer}_1 pointer1 指向 null \text{null} null

指针 pointer 2 \textit{pointer}_2 pointer2 的遍历过程和指针 pointer 1 \textit{pointer}_1 pointer1 的遍历过程相似,区别在于指针 pointer 2 \textit{pointer}_2 pointer2 先遍历链表 listB \textit{listB} listB 后遍历链表 listA \textit{listA} listA

两个指针的遍历同步进行,每次两个指针同时移动,直到两个指针指向同一个结点或者同时指向 null \text{null} null

  • 如果两个指针指向同一个结点,则该结点即为链表相交的起始结点,返回该结点。

  • 如果两个指针同时指向 null \text{null} null,则两个链表不相交,返回 null \text{null} null

证明

双指针解法的正确性证明需要考虑两种情况,第一种情况是两个链表相交,第二种情况是两个链表不相交。

如果两个链表相交,假设两个链表在相交之前的部分各有 x x x 个结点和 y y y 个结点,共用的部分有 z z z 个结点,则 x + z = m x + z = m x+z=m y + z = n y + z = n y+z=n,其中 m m m n n n 分别是链表 listA \textit{listA} listA listB \textit{listB} listB 的结点数。

  • 如果 x = y x = y x=y,则两个指针同时到达链表相交的起始结点。

  • 如果 x ≠ y x \ne y x=y,则指针 pointer 1 \textit{pointer}_1 pointer1 headA \textit{headA} headA 开始移动 m + y + 1 m + y + 1 m+y+1 次之后到达链表相交的起始结点(从 null \text{null} null 移动到 headB \textit{headB} headB 也是一次移动),指针 pointer 2 \textit{pointer}_2 pointer2 headB \textit{headB} headB 开始移动 n + x + 1 n + x + 1 n+x+1 次之后到达链表相交的起始结点(从 null \text{null} null 移动到 headA \textit{headA} headA 也是一次移动),由于 m + y + 1 = n + x + 1 = x + y + z + 1 m + y + 1 = n + x + 1 = x + y + z + 1 m+y+1=n+x+1=x+y+z+1,因此两个指针同时到达链表相交的起始结点。

如果两个链表不相交,则两个指针一定会在移动相同次数之后同时指向 null \text{null} null

  • 如果 m = n m = n m=n,则两个链表的结点数相同,两个指针遍历两个链表同时结束,因此同时指向 null \text{null} null

  • 如果 m ≠ n m \ne n m=n,则指针 pointer 1 \textit{pointer}_1 pointer1 headA \textit{headA} headA 开始移动 m + n + 1 m + n + 1 m+n+1 次之后指向 null \text{null} null(从 null \text{null} null 移动到 headB \textit{headB} headB 也是一次移动),指针 pointer 2 \textit{pointer}_2 pointer2 headA \textit{headA} headA 开始移动 n + m + 1 n + m + 1 n+m+1 次之后指向 null \text{null} null(从 null \text{null} null 移动到 headA \textit{headA} headA 也是一次移动),由于 m + n + 1 = n + m + 1 m + n + 1 = n + m + 1 m+n+1=n+m+1,因此两个指针同时指向 null \text{null} null

代码

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode pointer1 = headA, pointer2 = headB;
        while (pointer1 != pointer2) {
            pointer1 = pointer1 == null ? headB : pointer1.next;
            pointer2 = pointer2 == null ? headA : pointer2.next;
        }
        return pointer1;
    }
}

复杂度分析

  • 时间复杂度: O ( m + n ) O(m + n) O(m+n),其中 m m m n n n 分别是链表 listA \textit{listA} listA listB \textit{listB} listB 的结点数。最坏情况下,每个指针遍历两个链表各一次。

  • 空间复杂度: O ( 1 ) O(1) O(1)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

伟大的车尔尼

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

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

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

打赏作者

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

抵扣说明:

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

余额充值