相交链表:走完对方的路,终点自然对齐
交点是同一个节点对象,不是两个数字碰巧一样。独有前缀不一样长,就让两条路径都走「自己 + 对方」。
相交的含义是内存里的同一块节点,不是 Val 相等。双指针 pA、pB 分别从 headA、headB 出发,走完本链就跳到对方链头。二者总路程同为 lenA+lenB:若相交,会在共享后缀的第一格相遇;若不相交,会同时变成 nil。时间 O(m+n),空间 O(1)。
这是 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 绝不能返回。任一头为空,一开始就可以断定没有交点。每个节点最多被每个指针经过两次,时间是两条链长度之和,额外两个指针。
Go:双指针互换赛道
func getIntersectionNode(headA, headB *ListNode) *ListNode {pA, pB := headA, headBfor 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。走到空再跳,不要提前跳。
- 量长度再让长的先走差额,和换链是同一件事。哈希能验答案,但用空间换掉了对齐,不是这道题该停的解。