当前:LC160 · 相交链表 · 首次出现于 Day 10 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC160 · Intersection of Two Linked Lists · 链表

相交链表:走完对方的路,终点自然对齐

交点是同一个节点对象,不是两个数字碰巧一样。独有前缀不一样长,就让两条路径都走「自己 + 对方」。

相交的含义是内存里的同一块节点,不是 Val 相等。双指针 pA、pB 分别从 headA、headB 出发,走完本链就跳到对方链头。二者总路程同为 lenA+lenB:若相交,会在共享后缀的第一格相遇;若不相交,会同时变成 nil。时间 O(m+n),空间 O(1)。

时间 O(m+n)空间 O(1)结论先行 · 全文约 6 节
导读

这是 LeetCode 160. Intersection of Two Linked Lists。大白话:给你两条单链表的头节点 headA 和 headB,如果它们在某个节点开始相交,返回那个交点;如果不相交,返回空。

相交不是「两个节点上的数字碰巧一样」,而是两条链从某一格开始变成了同一个节点对象:改这一格的 Next,两条链后面都会跟着变。单链表每个节点只有一根 Next,相交之后不能再分开,形状只能是 Y,不能是 X。

主例:A 是 4 → 1 → 8 → 4 → 5,B 是 5 → 6 → 1 → 8 → 4 → 5,交点是值为 8 的那个节点。务必看清:A 里那个 1 和 B 里那个 1 数字相同,但它们是两个节点。如果只比较 Val,会在 B 的 1 上误报。缺的是对齐——独有前缀不一样长,直接同步走碰不到。

先想想:对齐需要什么

给主例每个节点起地址,避免被值骗。A 独有 A4、A1;B 独有 B5、B6、B1;共享 C8、C4、C5。A1 和 B1 值都是 1,地址不同。交点是 C8。若两链长度相同,两个指针同步走,第一个相同节点就是交点。这里 A 长 5、B 长 6,同步走时一方还在自己的前缀里,另一方已经踏进共享区,比较没有意义。

假设交点存在。A 的独有前缀长度是 a,B 的独有前缀长度是 b,共享后缀长度是 c。从两个头同时往前走,先走完独有前缀的那一方会先踏进共享区。必须让长的那条先空走 |a − b| 步,把差额抹平,再并排走,才会在共享区的第一格碰头。主例差额是 1,B 先走掉 B5,再从 A4 与 B6 并排:下一步 A1 对 B1,值相等但地址不同,继续;再下一步两人同时到 C8。

如果不相交,c = 0,两条链没有共享节点。对齐之后并排走,会一起走到空,返回空。哈希表也能做:把 A 的每个节点地址放进集合,再扫 B,第一个已经在集合里的就是交点。正确,但把题目从「怎么对齐两条链」换成了「怎么记住见过的节点」,空间换掉了对齐。

起跑线长度差几步,就有一个指针永远慢几拍。对齐不是让它们从同一个头出发,而是让它们剩下的路程一样长。

互换赛道:总路程相同

量长度再对齐,推理更直:先扫出 5 和 6,让 B 空走 1 步,再并排。换链写法不先数长度,靠路程相等完成同一件事。pA 从 A 头出发,走到空之后改从 B 头继续;pB 从 B 头出发,走到空之后改从 A 头继续。pA 走过的路程是 lenA + lenB,pB 走过的路程是 lenB + lenA,一样长。

若相交,把路程拆开:pA 先走完 a+c,再走 B 的前缀 b,总共 a+c+b,下一步踏进共享第一格;pB 先走完 b+c,再走 A 的前缀 a,总共也是 b+c+a,下一步同样踏进 C8。两人在交点第一次重合。主例 pA 的路径是 A4 A1 C8 C4 C5 | B5 B6 B1 C8,pB 的路径是 B5 B6 B1 C8 C4 C5 | A4 A1 C8,竖线两边都是 8 步之后踏进 C8。

若不相交,两人会在走完 lenA+lenB 之后同时变成 nil,nil 等于 nil,循环结束,返回空。把空当成「虚拟交点」,不相交也被同一套比较接住。走到空的那一步必须真的走到 nil 再跳,不要在最后一个节点就提前跳到对方——少走「空」这一格,两条路径的长度不再相等,不相交时可能死循环,相交时也会错位。

比较的是指针,不是 Val。主例并排走到 A1 和 B1 时,两个 1 绝不能返回。任一头为空,一开始就可以断定没有交点。每个节点最多被每个指针经过两次,时间是两条链长度之和,额外两个指针。

pA 走完 A 走 B,pB 走完 B 走 A
A
4
1
8
4
5
B
5
6
1
8
4
5
pA 在 4,pB 在 5

Go:双指针互换赛道

solution.goGo
func getIntersectionNode(headA, headB *ListNode) *ListNode {
pA, pB := headA, headB
for pA != pB {
if pA == nil { pA = headB } else { pA = pA.Next }
if pB == nil { pB = headA } else { pB = pB.Next }
}
return pA
}

1两指针各自从本链头出发。循环条件是 pA != pB,比较的是节点地址,不是 Val。

2pA == nil 时才跳到 headB,不要写成 pA.Next == nil 时提前跳。少走空这一拍,路程不再相等。

3走完本链就去走对方的前缀,等于用对方的独有段把长度差补上。相交时下一步同时踏进共享第一格。

4无交点时二者同时变成 nil,nil == nil,循环结束,返回 nil。空被同一套比较接住,不必另写分支。

总结

交点是同一个节点对象;让两条路径都走「自己 + 对方」,对齐处不是交点就是一起走到的空。

  • A 里的 1 和 B 里的 1 可以值相等、地址不同。必须比较指针,用 Val 会在主例误报。
  • 互换赛道让 lenA+lenB 一样长。有交点相遇在共享第一格;无交点同时到 nil。走到空再跳,不要提前跳。
  • 量长度再让长的先走差额,和换链是同一件事。哈希能验答案,但用空间换掉了对齐,不是这道题该停的解。
同族题目
LC141环形链表LC142环形链表 IILC19删除链表的倒数第 N 个结点