LeetCode 面试题 02.07. 链表相交
题目描述
题意分析
给两条单链表的头节点,判断它们是否共用同一段尾部;如果是,返回共用部分的第一个节点,否则返回空。
最要紧的一句约束是「相交」的定义:指的是两条链在内存中共享同一个节点对象,而不是恰好有值相等的节点。这直接决定了所有比较都必须用引用相等,任何比较
val的写法都是错的。举个例子,两条链里各有一个值为1的节点,它们可能是完全不同的两个对象。由这个定义还能推出一个重要的结构性质:单链表每个节点只有一个
next,所以一旦两条链在某个节点汇合,之后就再也分不开了。也就是说相交部分必然是一段公共后缀,不可能出现「合了又分」。第二个信号是进阶要求:时间 $O(m + n)$、空间 $O(1)$。这堵死了「把一条链的节点全塞进哈希集合再查另一条」这种最省事的写法。
边界要想清楚三处:任一链表为空时必然无交点;两条链长度可以相差很大;两条链完全不相交时要能自然终止并返回空,而不是死循环。题目还保证链表中无环。
解法:双指针交换链表头
核心思路
先看两个基线解法。一是哈希法:把
headA的所有节点存进集合,再沿headB逐个查,第一个命中的就是交点。正确且直观,但空间 $O(m)$,不满足进阶。二是长度对齐法:先各扫一遍求出两条链的长度,让长的那条先走|m - n|步抵消差值,然后同步前进,相等即为交点。空间是 $O(1)$ 了,但要写求长度、算差值、按大小分支走,代码分支多,白板上容易在「谁更长」这里写反。瓶颈其实只有一个:两条链的长度差。前缀不等长,两个指针同步走就永远对不齐。长度对齐法是显式地把差值算出来消掉,那有没有办法让它自动消掉?
关键观察是:如果让指针
curA先走完A再接着走B,让curB先走完B再接着走A,两者走过的总长度都是m + n。设两条链的独有前缀长度分别是a和b、公共后缀长度是c(于是m = a + c、n = b + c)。curA到达交点时走了a + c + b步,curB到达交点时走了b + c + a步——完全相同。换头这个动作,恰好用「多走一遍对方的前缀」抵消了长度差。不变量是:两个指针每轮各前进一步,走过的总步数始终相等;当两者都进入公共后缀时,它们距离链尾的剩余长度也相等,因此第一次引用相等发生的位置必然就是公共后缀的起点。 若两条链不相交,可以把「空指针」看成一个虚拟的公共终点:
curA在第m + n + 1步、curB也在第m + n + 1步同时变为空,此时curA == curB成立,循环退出并返回空,无需任何额外分支。
解题步骤
- 令
curA = headA、curB = headB,两个指针从各自链头出发。- 循环条件写成
curA != curB。用引用比较而不是值比较,是题目定义决定的;同时这个条件天然兼容「两者同时为空」的收敛情形。- 每轮先处理
curA:若它已经是空,就跳到headB;否则正常前进一步。判空必须在解引用之前,否则走到链尾会崩。- 同样地处理
curB:为空则跳到headA,否则前进一步。两个指针的推进必须在同一轮里各做一次,快慢不一致会破坏「总步数相等」这个不变量。- 注意换头的时机是「当前指针为空」,而不是「当前指针的
next为空」。前者让指针在切换前额外经过了一次空位置,正是这一步让不相交的情形能够同时收敛到空。- 循环结束后直接返回
curA。它要么是交点,要么是空,两种情况共用同一个出口。以
listA = [4,1,8,4,5]、listB = [5,6,1,8,4,5]、交点是值为8的那个节点走一遍:把A的节点记作A0..A4,B的记作B0..B5,其中A2、A3、A4与B3、B4、B5是同一批对象,交点是A2(也就是B3)。初始curA = A0、curB = B0,引用不同。第一轮后curA = A1、curB = B1。第二轮后curA = A2、curB = B2,注意此时curA已经是交点但curB还没到,不能停。第三轮后curA = A3、curB = B3,两者都在公共段里,但位置错开,引用仍不同。第四轮后curA = A4、curB = B4。第五轮后curA走出A的末尾变成空,curB = B5。第六轮,curA为空所以换头到B0,curB走出B的末尾变成空。第七轮,curA = B1,curB为空所以换头到A0。第八轮,curA = B2,curB = A1;虽然这两个节点的值都是1,但它们是不同对象,引用比较结果为假,循环继续——这一步正是「不能比值」的活例子。第九轮,curA = B3,curB = A2,而B3与A2是同一个对象,条件不成立,循环退出,返回值为8的交点节点。整个过程两个指针各走了九步,完全同步。
代码实现
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode curA = headA;
ListNode curB = headB;
// 两个指针各走两条链表,总路程一致后自然消除长度差。
while (curA != curB) {
if (curA == null) {
curA = headB;
} else {
curA = curA.next;
}
if (curB == null) {
curB = headA;
} else {
curB = curB.next;
}
}
return curA;
}
}
func getIntersectionNode(headA, headB *ListNode) *ListNode {
curA := headA
curB := headB
// 两个指针各走两条链表,总路程一致后自然消除长度差。
for curA != curB {
if curA == nil {
curA = headB
} else {
curA = curA.Next
}
if curB == nil {
curB = headA
} else {
curB = curB.Next
}
}
return curA
}
复杂度分析
- 时间复杂度:$O(m + n)$,其中 $m$、$n$ 是两条链的长度。相交时两个指针最多各走
a + b + c步,不相交时最多各走m + n + 1步,都是两条链长度之和的量级。- 空间复杂度:$O(1)$,只用了
curA、curB两个指针,没有哈希集合也没有递归栈,满足进阶要求。
关键点总结
- 处理两个不等长序列的对齐问题,除了显式算差值,还可以让两者互换赛道跑满同样的总路程,让差值自动抵消。这个技巧在「两个链表求公共部分」「两个数组求同位置关系」一类题里都能复用。
- 把「不相交」看成「在虚拟的空节点处相交」,就能让正常出口和失败出口合并成同一条。凡是能把特例吸收进主逻辑的设计,都能显著减少分支和出错面。
- 换头判据必须是「指针本身为空」而不是「指针的
next为空」。差这一步,两个指针的总步数就会差1,不相交时会错过同时为空的时刻,直接陷入死循环。- 引用相等和值相等是两回事。链表题里凡是涉及「同一个节点」的判定,一律用引用比较,这是题意层面的硬性要求。
- 面试视角:完整的展示顺序是哈希法(点明 $O(m)$ 空间)、长度对齐法(点明分支多)、换头法($O(1)$ 空间且无分支),并给出
a + c + b = b + c + a这个等式作为正确性依据。能写出这个等式,面试官基本不会再追问。- 面试视角:常见追问是「如果链表可能有环怎么办」。要能答:先各自判环,两条链的成环状态不同则必不相交;都无环退化为本题;都有环则需比较环入口是否相同,或让一条链的入口沿环走一圈看是否遇到另一条的入口。
易错点总结
- 错误写法:循环条件写成
curA.val != curB.val。用例listA = [4,1,8,4,5]、listB = [5,6,1,8,4,5]→ 走到curA = B2、curB = A1时两者值都是1,会误判为交点并返回错误节点。- 错误写法:换头判据写成
if (curA.next == null) curA = headB;。用例两条不相交的链[1]和[2]→ 两个指针永远不会同时为空,条件curA != curB恒成立,程序陷入死循环。- 错误写法:先判空再解引用的顺序写反,例如写成
curA = curA.next; if (curA == null) curA = headB;且没有前置判空。用例listA = [1]→ 第二轮对空指针取next,直接抛空指针异常。- 错误写法:换头时切回自己的链头,写成
curA = headA。用例listA = [4,1,8,4,5]、listB = [5,6,1,8,4,5]→ 两个指针各自在自己的链上循环,长度差永远消不掉,不相交时死循环,相交时也可能永远错开。- 错误写法:一轮里只推进其中一个指针,例如把
curB的更新写进else分支。用例任意长度不等的输入 → 总步数不再相等,「同时到达交点」的推理失效,结果随机出错。- 错误写法:给换头加计数限制,比如「最多换一次头就退出返回空」,但退出判断写在换头之后、比较之前。用例两条恰好在最后一个节点相交的链 → 在指针有机会相遇之前就提前返回空,漏判交点。
- 错误写法:认为返回值不可能为空,于是在外层直接访问返回节点的
val。用例listA = [2,6,4]、listB = [1,5](不相交)→ 返回空后解引用崩溃;本题无交点是合法结果。- 错误写法:把循环条件写成
curA != null && curB != null。用例listA = [4,1,8,4,5]、listB = [5,6,1,8,4,5]、交点为8→ 走满五步后curA变成空,循环直接退出并返回空,两个指针根本没机会进入第二条链,真实交点被漏掉。- 错误写法:用长度对齐法时把「谁先走」判反,让短链先走差值步。用例
listA长5、listB长6→ 短链提前走出末尾,同步比较阶段两个指针始终错位,交点被跳过。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 160. 相交链表 | 简单 | 同一模型的主站编号,可用来检验模板是否稳定 |
| LCR 023. 相交链表 | 简单 | 同题的国内版,适合对比不同判题下的空输入约定 |
| 剑指 Offer 52. 两个链表的第一个公共节点 | 简单 | 同题换表述,重点仍是引用相等而非值相等 |
| 141. 环形链表 | 简单 | 快慢指针判环,考察相遇条件而非对齐长度 |
| 142. 环形链表 II | 中等 | 相遇后用数学推导定位环入口,需要额外一轮同步 |
| 876. 链表的中间结点 | 简单 | 一趟遍历定位中点,考察奇偶长度下的停止条件 |
| 287. 寻找重复数 | 中等 | 把数组下标视作链表边,把找重复转成找环入口 |