hot100 相交链表(160)

本题采用双指针错位交换遍历算法解决"相交链表"的判定与交点检索问题。其核心本质是通过逻辑拼接消除两个链表长度不一致的拓扑差异,使双指针在第二轮遍历中达成步调一致。当前源码实现了时间复杂度 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)

  • 分析 :算法运行期间,没有创建任何与输入规模相关的链表克隆副本、没有开辟任何哈希映射表,仅仅是在栈内存中独立申请了 pq 两个引用类型的局部指针变量。

  • 结论:程序执行所分配的额外内存开销恒定,不随链表节点数量的扩大而产生任何波动,符合常数阶原地算法的标准定义。