链表相交问题看似简单,实则蕴含着精妙的数据结构特性。要真正掌握这个问题,我们需要从最基础的链表结构说起。
单链表由一系列节点组成,每个节点包含两个部分:存储的数据(val)和指向下一个节点的指针(next)。这种结构决定了链表的一个重要特性——一旦两个链表在某节点相交,那么从这个节点开始,后续所有节点必然完全相同。这是因为每个节点只能有一个next指针,不可能出现分叉的情况。
关键理解:链表相交不是简单的数值相同,而是内存地址相同。两个链表可能在多个节点存储相同的数值,但只有内存地址相同的节点才是真正的相交点。
在实际编程面试中,面试官通常会给出这样的测试用例:
最直观的解法是使用哈希表:
这种方法时间复杂度O(m+n),空间复杂度O(m)或O(n),取决于哪个链表更长。虽然能解决问题,但需要额外的存储空间,不是最优解。
另一种常见方法是:
这种方法虽然空间复杂度降到了O(1),但仍需要两次完整遍历来计算长度,代码实现也相对复杂。
双指针法之所以高效,是因为它利用了数学上的对称性。让我们用生活中的例子来理解:
想象两个人在不同的跑道上跑步:
如果两人分别从各自起点出发,速度相同:
设:
指针pA的路径:a→c→b→c
指针pB的路径:b→c→a→c
可以看到,在走完a+b+c步后,两个指针都会到达共享段的起点。如果两链表不相交,则会在走完a+b步后同时变为nullptr。
cpp复制class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
// 边界条件处理:任一链表为空则不可能相交
if (!headA || !headB) return nullptr;
ListNode *pA = headA, *pB = headB;
// 主循环
while (pA != pB) {
// pA到达末尾后转到headB继续
pA = pA ? pA->next : headB;
// pB到达末尾后转到headA继续
pB = pB ? pB->next : headA;
}
// 返回相遇点(可能为nullptr表示不相交)
return pA;
}
};
当面试官提出这个问题时,建议按照以下步骤回答:
面试官可能会追问:
虽然这个问题看起来是纯理论性的,但实际应用广泛:
如果链表可能有环,需要先检测环的存在:
对于k个链表的相交问题,可以:
如果链表节点有权值,寻找相交点后权值和最大的节点,可以在找到相交点后额外遍历共享段。
完善的测试应该包括:
虽然算法已经很高效,但仍可以:
python复制def getIntersectionNode(headA, headB):
pA, pB = headA, headB
while pA != pB:
pA = pA.next if pA else headB
pB = pB.next if pB else headA
return pA
Python实现更简洁,但原理完全相同。
java复制public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode pA = headA, pB = headB;
while (pA != pB) {
pA = (pA != null) ? pA.next : headB;
pB = (pB != null) ? pB.next : headA;
}
return pA;
}
Java实现需要注意null检查,避免NullPointerException。
解决链表相交问题的关键在于理解数据结构的本质特性。双指针法之所以优雅,是因为它利用了路径长度的数学对称性,而不需要额外的存储空间或预处理。
在实际编码中,我发现了几个值得注意的点:
这个问题教会我们,有时候最优解不是最直观的解法,而是需要对问题本质有深刻理解后发现的巧妙方法。在面试中展示这种深入思考过程,往往比直接给出答案更能体现你的算法能力。