🧱 LeetCode 踩坑日志:相交链表 (LC160)

27 阅读3分钟

1. 从物理观察到分析推导

  • 题目分析
    观察物理结构: 如果两条单向链表相交,它们必然呈现 “Y型结构”

    基于物理结构的推导,单向通行路径共享。一旦两个节点在某一点汇合,剩下的路程(直到尾部)对于两者来说是完全一样的。

    这意味着:相交点之后的长度相等,且尾节点相同。

  • 当前阻碍
    为什么简单地“一起走”不行?
    因为两条链表的 “起跑线”不同(头部长度不一致)。
    如果 A 链长 10,B 链长 8,两个指针同时出发,当 B 到达交点时,A 还在两步之外。这种相位差(Phase Shift)导致我们无法在交点相遇。

2. 踩坑记录

在做题过程中,我们通过画出数据结构的图可以很容易地得到一个清晰的结论,两条存在相交点的单链表,会形成一个Y型结构,且从相交点开始一直到尾巴节点,两条链表是重合的,是相同的。 所以我们可以从一个相对笨拙的方法入手,先遍历对比结尾点是否相同,可以得出是否有相交点,同时还可以记录每一条链表的长度,方便我们后续找节点的时候对齐两条链表开头的长度。

3. 适配器策略

这里我们用最朴素的工程测量法解决问题。

  • 核心策略测量 -> 对齐 -> 同步

  • 积木调用

    1. 首先调用  [线性遍历]  的变体(全量度量),获取两条链表的物理属性(长度 & 尾节点)。
    2. 根据长度差,调用  [差值对齐] ,消除起跑线差异。
    3. 最后再次使用  [线性遍历]  进行同步比对。

4. 代码组装

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     val: number
 *     next: ListNode | null
 *     constructor(val?: number, next?: ListNode | null) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.next = (next===undefined ? null : next)
 *     }
 * }
 */

// --- 积木定义区 ---

// 积木 A: [线性遍历] 变体 -> 获取长度和尾节点
function getListMetrics(head: ListNode | null): { length: number; tail: ListNode | null } {
    let len = 0;
    let curr = head;
    let tail = null;
    
    // 积木核心:标准的线性遍历骨架
    while (curr !== null) {
        len++;
        tail = curr; // 记录当前轨迹
        curr = curr.next;
    }
    return { length: len, tail: tail };
}

// 积木 B: [差值对齐] -> 让指针先行 delta 步
function advancePointer(node: ListNode | null, delta: number): ListNode | null {
    let curr = node;
    while (delta > 0 && curr !== null) {
        curr = curr.next;
        delta--;
    }
    return curr;
}

// --- 主逻辑区 ---

function getIntersectionNode(headA: ListNode | null, headB: ListNode | null): ListNode | null {
    if (!headA || !headB) return null;

    // Step 1: 物理勘测 (利用积木 A)
    const metricsA = getListMetrics(headA);
    const metricsB = getListMetrics(headB);

    // 核心剪枝:如果尾巴不同,说明是平行线,物理上不可能相交
    if (metricsA.tail !== metricsB.tail) {
        return null;
    }

    // Step 2: 计算相位差
    let ptrA = headA;
    let ptrB = headB;
    const delta = Math.abs(metricsA.length - metricsB.length);

    // Step 3: 相位对齐 (利用积木 B)
    // 谁长谁先跑,跑完之后大家距离终点就一样远了
    if (metricsA.length > metricsB.length) {
        ptrA = advancePointer(ptrA, delta);
    } else {
        ptrB = advancePointer(ptrB, delta);
    }

    // Step 4: 同步查找
    // 此时回到最基础的 [线性遍历] 模式,同步步进
    while (ptrA !== ptrB) {
        ptrA = ptrA!.next;
        ptrB = ptrB!.next;
    }

    return ptrA; // 相遇点即为交点
}

5. 相关算法积木

积木 A:全量度量器 (The Metric Scanner)

可以看做是 🧊 算法积木:单链表的「线性遍历」  的一种变体。标准遍历只走过程,而这个变体目的是采集数据

  • 输入:链表头。
  • 输出:{ length, tail }。
  • 作用:根据“同尾必相交”原理进行快速剪枝,并提供长度数据。

积木 B:差值对齐器 (Gap Aligner)

直接调用  🧊 算法积木:单链表中的「差值对齐」

  • 输入:较长的链表头, 步数 delta。
  • 输出:对齐后的新起点。
  • 作用:将“追击问题”转化为“相遇问题”。