本题采用双指针错位交换遍历算法解决"相交链表"的判定与交点检索问题。其核心本质是通过逻辑拼接消除两个链表长度不一致的拓扑差异,使双指针在第二轮遍历中达成步调一致。当前源码实现了时间复杂度 O(m + n) 与额外空间复杂度 O(1) 的物理极限方案,最终走向是精准定位首个相交节点内存地址或返回 null。
一、 问题本质与数学模型
设链表 A 的独立非相交部分长度为 a,链表 B 的独立非相交部分长度为 b,两个链表从交点到尾节点的公共相交部分长度为 c(若不相交则 c = 0)。
由此可得两个链表的总长度模型为:
-
链表 A 的总长度:m = a + c
-
链表 B 的总长度:n = b + c
如果 a != b,双指针分别从 A 和 B 的头节点出发同步向后移动,由于路程差的存在,它们永远不可能同时到达相交的起始节点。
为了消除这个长度差,算法引入了逻辑拼接模型。当指针 p 遍历完链表 A 的末尾时,将其重置到链表 B 的头部继续遍历;同理,当指针 q 遍历完链表 B 的末尾时,将其重置到链表 A 的头部。
-
指针 p 走过的总路程:(a + c) + b
-
指针 q 走过的总路程:(b + c) + a
因为 (a + c) + b = (b + c) + a,在数学上两者的前进距离完全相等。当两个指针分别走完这段对等物理路程时,它们将同时到达相交的起始节点。
二、 算法演进对比
在解决链表拓扑交集检索问题时,错位交换法在时空资源的利用效率上表现最优:
| 解法名称 | 时间复杂度 | 空间复杂度 | 核心原理 | 物理瓶颈 / 缺陷 |
|---|---|---|---|---|
| 暴力双重循环 | O(m * n) | O(1) | 遍历链表 A 的每个节点,内部嵌套循环遍历整个链表 B 进行地址比对 | 时间开销呈平方阶,极端低效 |
| 哈希表辅助法 | O(m + n) | O(m) 或 O(n) | 先遍历链表 A,将所有节点的内存地址存入 HashSet,再遍历链表 B 检索碰撞 | 引入了与链表规模成正比的额外内存开销,非原地算法 |
| 长度差同步步进法 | O(m + n) | O(1) | 先分别统计两链表长度,算出差值 k,让长链表先走 k 步,随后双指针同步前进 | 需要对链表执行多次显式的完备扫描,代码逻辑分支较多 |
| 错位交换法(当前解法) | O(m + n) | O(1) | 隐式利用对等路程和,指针走完自身后转向对方头部,实现自适应对齐 | 无,为当前时空复杂度与工程实现的绝对极限 |
三、 核心分支控制逻辑与决策证明
当前源码的控制流完全依托于一行简纯的 while (p != q) 循环体。其内部的指针重定向机制证明如下:
1. 相交情况下的收敛性证明
-
第一轮遍历 :若 a != b,指针 p 和 q 无法同时到达交点。较短链表的指针会先触底(变为
null),随后被重定向到另一条长链表的头部。 -
第二轮遍历 :当两个指针都完成了头尾互换后,p 已经走了
(a + c)的长度并切换到 B 走了 b 步;q 已经走了(b + c)的长度并切换到 A 慢速前进了 a 步。此时它们刚好在交点 c1 处发生地址碰撞,满足p == q,退出循环并返回交点。
2. 不相交情况下的退出机制(c = 0)
-
数学表达:若两链表完全独立互不相交,则 c = 0。链表 A 长度为 a,链表 B 长度为 b。
-
逻辑演进 :指针 p 走完 A(a 步)后转向 B,走完 B(b 步)后总步数为
a + b;指针 q 走完 B(b 步)后转向 A,走完 A(a 步)后总步数同样为b + a。 -
结论 :在执行完第
a + b步时,指针 p 刚好走完了链表 B 的末尾节点,其下一跳变为null;而指针 q 也刚好走完了链表 A 的末尾节点,其下一跳同样变为null。此时触发p == q == null,循环条件破裂退出,函数正确返回null。
四、 算法执行状态机步进示例
以输入数据 链表 A = [4, 1, 8, 4, 5](独立长度 a=2),链表 B = [5, 6, 1, 8, 4, 5](独立长度 b=3),公共交点段 [8, 4, 5] 为例。指针状态机的迭代演进如下表所示:
| 迭代步数 | 指针 p 当前指向的节点值 | 指针 q 当前指向的节点值 | 触发的状态切换动作 | 说明 |
|---|---|---|---|---|
| 初始 | 4 (A1) | 5 (B1) | 正常向后步进 | p != q |
| 1 | 1 (A2) | 6 (B2) | 正常向后步进 | p != q |
| 2 | 8 (公共1) | 1 (B3) | 正常向后步进 | p != q |
| 3 | 4 (公共2) | 8 (公共1) | 正常向后步进 | p != q |
| 4 | 5 (公共3) | 4 (公共2) | 正常向后步进 | p != q |
| 5 | null (A 结束) | 5 (公共3) | p 触底,重定向到 headB | p != q |
| 6 | 5 (B1) | null (B 结束) | q 触底,重定向到 headA | p != q |
| 7 | 6 (B2) | 4 (A1) | 正常向后步进 | p != q |
| 8 | 1 (B3) | 1 (A2) | 正常向后步进 | p != q |
| 9 | 8 (公共1) | 8 (公共1) | 满足 p == q,终止循环 | 成功锁定交点位置 |
五、源码实现
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 边界安全检查:若任意链表为空,则绝对不可能存在相交节点
if (headA == null || headB == null) {
return null;
}
// 声明两个动态迭代指针,初始分别指向两链表头部
ListNode p = headA;
ListNode q = headB;
// 核心控制流:当 p 和 q 指向的节点内存地址相同时退出循环
// 若链表相交,退出时指向交点;若不相交,退出时两者同时为 null
while (p != q) {
// 指针 p 的三元控制逻辑:若未触底则继续后移,若触底则错位切换到链表 B 头部
p = (p != null) ? p.next : headB;
// 指针 q 的三元控制逻辑:若未触底则继续后移,若触底则错位切换到链表 A 头部
q = (q != null) ? q.next : headA;
}
// 返回最终达成一致的节点地址(交点或 null)
return p;
}
}
六、 复杂度分析
1. 时间复杂度:O(m + n)
-
最坏物理情况 :当两个链表不相交时,指针 p 完整遍历了链表 A 和链表 B,总计步进
m + n次;指针 q 完整遍历了链表 B 和链表 A,总计步进n + m次。 -
结论 :总的基本步进指令消耗与两个链表的长度加和(
m + n)呈严格的线性正比关系,且每个节点的内存寻址操作均为 O(1) 阶。
2. 空间复杂度:O(1)
-
分析 :算法运行期间,没有创建任何与输入规模相关的链表克隆副本、没有开辟任何哈希映射表,仅仅是在栈内存中独立申请了
p和q两个引用类型的局部指针变量。 -
结论:程序执行所分配的额外内存开销恒定,不随链表节点数量的扩大而产生任何波动,符合常数阶原地算法的标准定义。