【LeetCode】剑指 Offer 52. 两个链表的第一个公共节点 - Go语言题解

文章目录

  • 一、题目描述
  • 二、两个要点
    • 1. 判断节点相同的条件
    • 2. 对 “第一个公共节点” 的理解
  • 三、我的题解 - 遍历法
  • 四、哈希表法
  • 五、双指针


一、题目描述

输入两个链表,找出它们的 第一个公共节点

如下面的两个链表:

【LeetCode】剑指 Offer 52. 两个链表的第一个公共节点 - Go语言题解_第1张图片

在节点 c1 开始相交。

示例 1:

【LeetCode】剑指 Offer 52. 两个链表的第一个公共节点 - Go语言题解_第2张图片
输入: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:

【LeetCode】剑指 Offer 52. 两个链表的第一个公共节点 - Go语言题解_第3张图片
输入: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:
【LeetCode】剑指 Offer 52. 两个链表的第一个公共节点 - Go语言题解_第4张图片

输入: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。

注意:

  • 如果两个链表没有交点,返回 null。
  • 在返回结果后,两个链表仍须保持原有的结构。
  • 可假定整个链表结构中没有循环。
  • 程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。
  • 本题与主站 160 题相同:https://leetcode-cn.com/problems/intersection-of-two-linked-lists/

二、两个要点

1. 判断节点相同的条件

注意!!!

自两个链表的第一个公共节点开始,两个链表的后续节点不仅值相同,所有节点都是同一个东西!(我是按这个理解来的哈)

也就是说节点本身的存储位置(在内存中的位置)相同、节点的值相同、节点的 next 域相同。通过指向节点的指针(节点地址)就可以判断两个节点是不是同一个。

比如说下图:
【LeetCode】剑指 Offer 52. 两个链表的第一个公共节点 - Go语言题解_第5张图片

如果你光凭值来判断,很容易将节点 1 误认为是两链的公共节点。但是!8 才是,1 不是。

如何来区分两个不同的 1 节点?这两个 1 节点值相同,next 域也相同。不同的是两个节点本身的存储地址(内存所在的位置)!

所以通过指向它们的指针就可以判断(节点4的 next 域和节点0的 next 域)。

2. 对 “第一个公共节点” 的理解

其实,你只要找到了第一个公共节点。不用多加判断,两个链从第一个公共节点开始的后续所有节点都是一样的。

因为链表只有一个 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

}

评判结果:
【LeetCode】剑指 Offer 52. 两个链表的第一个公共节点 - Go语言题解_第6张图片

这个代码显然时间复杂度很高。因为遍历 A 链、反复遍历 B 链,时间复杂度为 O(n2)。

这也太慢啦,我们要想办法改进:以下介绍哈希表法。


四、哈希表法

思路和算法:

判断两个链表是否相交,可以使用哈希集合存储链表节点。

首先遍历链表A,并将链表 A 中的每个节点加入哈希集合中。然后遍历链表 B,对于遍历到的每个节点,判断该节点是否在哈希集合中:

  • 如果当前节点不在哈希集合中,则继续遍历下一个节点;
  • 如果当前节点在哈希集合中,则后面的节点都在哈希集合中,即从当前节点开始的所有节点都是两个链表的公共节点,因此在链表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
    }
   

}

评判结果:

【LeetCode】剑指 Offer 52. 两个链表的第一个公共节点 - Go语言题解_第7张图片
嘻嘻嘻,时间复杂度降低啦!

复杂度分析:

  • 时间复杂度: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
}

评判结果:
【LeetCode】剑指 Offer 52. 两个链表的第一个公共节点 - Go语言题解_第8张图片


五、双指针

官方还给了一种算法:双指针法。

思路和算法:

使用双指针,可以将空间复杂度降至 O(1)。

只有当链表 A 和 B 都不为空时,两个链表才可能相交。因此首先判断链表 A 和 B 是否为空,如果其中至少有一个链表为空,则两个链表一定不相交,返回 null。

当链表 A 和 B 都不为空时,创建两个指针 pA 和 pB,初始时分别指向两个链表的头节点 headA 和 headB,然后将两个指针依次遍历两个链表的每个节点。具体做法如下:

  • 每步操作需要同时更新指针 pA 和 pB。
  • 如果指针 pA 不为空,则将指针 pA 移到下一个节点;如果指针 pB 不为空,则将指针 pB 移到下一个节点。
  • 如果指针pA 为空,则将指针 pA 移到链表 headB 的头节点;如果指针 pB 为空,则将指针 pB 移到链表 headA 的头节点。
  • 当指针 pA 和 pB 指向同一个节点或者都为空时,返回它们指向的节点或者 null。

证明:

下面提供双指针方法的正确性证明。考虑两种情况,第一种情况是两个链表相交,第二种情况是两个链表不相交。

情况一:两个链表相交

链表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 == nm != n 时,两个指针分别会如何移动:
如果 m == n ,则两个指针会同时到达两个链表的尾节点,然后同时变成空值 null,此时返回 null;
如果 m != n,则由于两个链表没有公共节点,两个指针也不会同时到达两个链表的尾节点,因此两个指针都会遍历完两个链表,在指针 pA 移动了 m+n 次、指针 pB 移动了 n+m 次之后,两个指针会同时变成空值 null,此时返回 null。

妙啊!!!代码大家可以自己去LeetCode看。

以后我也可以自己动手写写贴上来。

你可能感兴趣的:(LeetCode,链表,leetcode,golang)