输入两个链表,找出它们的 第一个公共节点 。
如下面的两个链表:
在节点 c1 开始相交。
示例 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 个节点。
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
解释:这两个链表不相交,因此返回 null。
注意:
注意!!!
自两个链表的第一个公共节点开始,两个链表的后续节点不仅值相同,所有节点都是同一个东西!(我是按这个理解来的哈)
也就是说节点本身的存储位置(在内存中的位置)相同、节点的值相同、节点的 next 域相同。通过指向节点的指针(节点地址)就可以判断两个节点是不是同一个。
如果你光凭值来判断,很容易将节点 1 误认为是两链的公共节点。但是!8 才是,1 不是。
如何来区分两个不同的 1 节点?这两个 1 节点值相同,next 域也相同。不同的是两个节点本身的存储地址(内存所在的位置)!
所以通过指向它们的指针就可以判断(节点4的 next 域和节点0的 next 域)。
其实,你只要找到了第一个公共节点。不用多加判断,两个链从第一个公共节点开始的后续所有节点都是一样的。
因为链表只有一个 Next 域(要么是下一个节点的地址,要么是 nil)。不会产生分叉之类的情况。
所以只要找到了第一个一样的节点,就找到了公共链的开始。
遍历两个链:固定 A 链一个元素,遍历 B 链中有没有和它一样的。如果没有,就在 A 链上后移一个元素,再遍历 B 链,寻找相同节点。
如果找到了一个相同节点,就返回 A 。
我的代码:
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func getIntersectionNode(headA, headB *ListNode) *ListNode {
if headA == nil || headB == nil {
return nil
}
for headA != nil {
currB := headB
for (currB != nil) && (currB != headA) {
currB = currB.Next
}
if currB == nil {
headA = headA.Next
continue
} else { //currB == headA
return headA
}
}
return nil
}
这个代码显然时间复杂度很高。因为遍历 A 链、反复遍历 B 链,时间复杂度为 O(n2)。
这也太慢啦,我们要想办法改进:以下介绍哈希表法。
思路和算法:
判断两个链表是否相交,可以使用哈希集合存储链表节点。
首先遍历链表A,并将链表 A 中的每个节点加入哈希集合中。然后遍历链表 B,对于遍历到的每个节点,判断该节点是否在哈希集合中:
如果链表 B 中的所有节点都不在哈希集合中,则两个链表不相交,返回 null。
我的代码:
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func getIntersectionNode(headA, headB *ListNode) *ListNode {
if headA == nil || headB == nil {
return nil
}
Hash_table := map[*ListNode]bool{}
for headA != nil { //将 headA 加入哈希表
Hash_table[headA] = true
headA = headA.Next
}
for (Hash_table[headB] != true) && (headB != nil){
headB = headB.Next
}
if headB == nil {
return nil
}else{
return headB
}
}
评判结果:
复杂度分析:
时间复杂度:O(m+n),其中 m 和 n 是分别是链表 A 和 B 的长度。需要遍历两个链表各一次。
空间复杂度:O(m),其中 m 是链表 A 的长度。需要使用哈希集合存储链表A 中的全部节点。
官方给的哈希表题解代码:
func getIntersectionNode(headA, headB *ListNode) *ListNode {
vis := map[*ListNode]bool{}
for tmp := headA; tmp != nil; tmp = tmp.Next {
vis[tmp] = true
}
for tmp := headB; tmp != nil; tmp = tmp.Next {
if vis[tmp] {
return tmp
}
}
return nil
}
官方还给了一种算法:双指针法。
思路和算法:
使用双指针,可以将空间复杂度降至 O(1)。
只有当链表 A 和 B 都不为空时,两个链表才可能相交。因此首先判断链表 A 和 B 是否为空,如果其中至少有一个链表为空,则两个链表一定不相交,返回 null。
当链表 A 和 B 都不为空时,创建两个指针 pA 和 pB,初始时分别指向两个链表的头节点 headA 和 headB,然后将两个指针依次遍历两个链表的每个节点。具体做法如下:
证明:
下面提供双指针方法的正确性证明。考虑两种情况,第一种情况是两个链表相交,第二种情况是两个链表不相交。
情况一:两个链表相交
链表headA 和 headB 的长度分别是 m 和 n。假设链表 headA 的不相交部分有 a 个节点,链表 headB 的不相交部分有 b 个节点,两个链表相交的部分有 c 个节点,则有 a+c=m ,b+c=n。
如果 a == b,则两个指针会同时到达两个链表的第一个公共节点,此时返回两个链表的第一个公共节点;
如果 a != b ,则指针 pA 会遍历完链表 headA,指针 pB 会遍历完链表 headB,两个指针不会同时到达链表的尾节点,然后指针 pA 移到链表 headB 的头节点,指针 pB 移到链表 headA 的头节点,然后两个指针继续移动,在指针 pA 移动了 a+c+b 次、指针 pB 移动了 b+c+a 次之后,两个指针会同时到达两个链表的第一个公共节点,该节点也是两个指针第一次同时指向的节点,此时返回两个链表的第一个公共节点。
情况二:两个链表不相交
链表 headA 和 headB 的长度分别是 m 和 n。考虑当 m == n
和 m != n
时,两个指针分别会如何移动:
如果 m == n ,则两个指针会同时到达两个链表的尾节点,然后同时变成空值 null,此时返回 null;
如果 m != n,则由于两个链表没有公共节点,两个指针也不会同时到达两个链表的尾节点,因此两个指针都会遍历完两个链表,在指针 pA 移动了 m+n 次、指针 pB 移动了 n+m 次之后,两个指针会同时变成空值 null,此时返回 null。
妙啊!!!代码大家可以自己去LeetCode看。
以后我也可以自己动手写写贴上来。