【Hot100|21-LeetCode 160. 相交链表】

LeetCode 160. 相交链表 - 完整解法详解

一、问题理解

问题描述

给你两个单链表的头节点 headAheadB,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null

示例

text

复制代码
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8(注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

要求

  • 时间复杂度:O(m+n),其中 m 和 n 是两个链表的长度

  • 空间复杂度:O(1)

  • 链表不允许修改

  • 链表中不存在环

二、核心思路:双指针法

基本思想

两个指针分别从两个链表的头节点开始遍历,当一个指针到达链表末尾时,将其重定向到另一个链表的头节点。如果两个链表相交,那么这两个指针最终会在相交节点相遇;如果不相交,两个指针会同时到达 null。

数学原理

设链表 A 的非公共部分长度为 a,链表 B 的非公共部分长度为 b,公共部分长度为 c。

  • 指针 p 走过的路径:a + c + b(如果相交)

  • 指针 q 走过的路径:b + c + a(如果相交)

    两个指针走过相同的总路径长度,因此会在相交点相遇。

三、代码逐行解析

方法一:双指针法(最优解)

Python 解法

python

复制代码
# Definition for singly-linked list.
class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None

class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
        # 初始化两个指针
        p, q = headA, headB
        
        # 当两个指针不相同时继续循环
        while p is not q:
            # 如果p到达链表末尾,重定向到headB
            p = p.next if p else headB
            # 如果q到达链表末尾,重定向到headA
            q = q.next if q else headA
        
        # 返回p(此时p和q相同,要么是相交节点,要么是None)
        return p
Java 解法

java

复制代码
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        // 初始化两个指针
        ListNode p = headA, q = headB;
        
        // 当两个指针不相同时继续循环
        while (p != q) {
            // 如果p到达链表末尾,重定向到headB
            p = (p == null) ? headB : p.next;
            // 如果q到达链表末尾,重定向到headA
            q = (q == null) ? headA : q.next;
        }
        
        // 返回p(此时p和q相同)
        return p;
    }
}

方法二:计算长度法

Python 解法

python

复制代码
class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
        # 步骤1:计算两个链表的长度
        def getLength(head):
            length = 0
            while head:
                length += 1
                head = head.next
            return length
        
        lenA = getLength(headA)
        lenB = getLength(headB)
        
        # 步骤2:让长链表的指针先走差值步
        p, q = headA, headB
        if lenA > lenB:
            for _ in range(lenA - lenB):
                p = p.next
        else:
            for _ in range(lenB - lenA):
                q = q.next
        
        # 步骤3:同时前进,直到找到相交节点或到达末尾
        while p and q and p != q:
            p = p.next
            q = q.next
        
        return p
Java 解法

java

复制代码
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        // 计算两个链表的长度
        int lenA = getLength(headA);
        int lenB = getLength(headB);
        
        // 让长链表的指针先走差值步
        ListNode p = headA, q = headB;
        if (lenA > lenB) {
            for (int i = 0; i < lenA - lenB; i++) {
                p = p.next;
            }
        } else {
            for (int i = 0; i < lenB - lenA; i++) {
                q = q.next;
            }
        }
        
        // 同时前进,直到找到相交节点
        while (p != null && q != null && p != q) {
            p = p.next;
            q = q.next;
        }
        
        return p;
    }
    
    private int getLength(ListNode head) {
        int length = 0;
        while (head != null) {
            length++;
            head = head.next;
        }
        return length;
    }
}

四、Java 与 Python 语法对比

1. 链表节点定义

操作 Java Python
节点类 需要预定义 需要预定义
创建节点 new ListNode(val) ListNode(val)

2. 空值判断

操作 Java Python
检查null node == null node is None
三元运算符 p = (p == null) ? headB : p.next; p = p.next if p else headB

3. 循环控制

操作 Java Python
while循环 while (p != q) while p is not q:
指针移动 p = p.next; p = p.next

五、实例演示

示例:两个链表相交

text

复制代码
链表A: 4 → 1 → 8 → 4 → 5
链表B: 5 → 6 → 1 ↗
                8 → 4 → 5
相交节点:值为8的节点
双指针法步骤:
  1. 初始化p = 节点4(A头), q = 节点5(B头)

  2. 第1轮

    • p ≠ q,继续

    • p = p.next = 节点1

    • q = q.next = 节点6

  3. 第2轮

    • p ≠ q,继续

    • p = p.next = 节点8

    • q = q.next = 节点1

  4. 第3轮

    • p ≠ q,继续

    • p = p.next = 节点4

    • q = q.next = 节点8

  5. 第4轮

    • p ≠ q,继续

    • p = p.next = 节点5

    • q = q.next = 节点4

  6. 第5轮

    • p ≠ q,继续

    • p到达A末尾,重定向到headB = 节点5

    • q = q.next = 节点5

  7. 第6轮

    • p ≠ q,继续

    • p = p.next = 节点6

    • q到达B末尾,重定向到headA = 节点4

  8. 第7轮

    • p ≠ q,继续

    • p = p.next = 节点1

    • q = q.next = 节点1

  9. 第8轮

    • p ≠ q,继续

    • p = p.next = 节点8

    • q = q.next = 节点8

  10. 第9轮

    • p = q = 节点8,返回节点8
路径长度分析:
  • 指针p路径:A(4,1,8,4,5) + B(5,6,1,8) = 9步

  • 指针q路径:B(5,6,1,8,4,5) + A(4,1,8) = 9步

  • 在节点8相遇

示例:两个链表不相交

text

复制代码
链表A: 1 → 2 → 3
链表B: 4 → 5
不相交
双指针法步骤:
  1. 第1-3轮:分别遍历各自链表

  2. 第4-5轮:交换链表继续遍历

  3. 最终pq同时到达null,返回null

六、关键细节解析

1. 为什么双指针法能保证相遇?

设链表A长度为a+c,链表B长度为b+c(c为公共部分长度)

  • 指针p路径:a + c + b(A链表 + 交换到B链表)

  • 指针q路径:b + c + a(B链表 + 交换到A链表)

  • 两个指针走过相同的总长度,因此会同时到达相交点或null

2. 如何处理不相交的情况?

当两个链表不相交时:

  • 指针p路径:m + n(遍历A链表 + 遍历B链表)

  • 指针q路径:n + m(遍历B链表 + 遍历A链表)

  • 两个指针最终会同时到达null,循环结束

3. 时间复杂度为什么是O(m+n)?

  • 每个指针最多遍历两个链表各一次

  • 总遍历次数:m + n

  • 时间复杂度:O(m+n)

4. 空间复杂度为什么是O(1)?

  • 只使用了两个指针变量

  • 没有使用额外的数据结构

  • 空间复杂度:O(1)

5. 如果链表有环怎么办?

题目假设链表没有环。如果有环,双指针法会陷入无限循环,需要先判断是否有环。

七、复杂度分析

方法一:双指针法

  • 时间复杂度:O(m+n)

    • 每个指针最多遍历两个链表各一次
  • 空间复杂度:O(1)

    • 只使用了常数个指针变量

方法二:计算长度法

  • 时间复杂度:O(m+n)

    • 计算长度:O(m) + O(n)

    • 对齐后遍历:O(min(m,n))

  • 空间复杂度:O(1)

    • 只使用了常数个指针变量

八、其他解法

解法一:哈希表法

python

复制代码
class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
        # 使用集合存储链表A的所有节点
        visited = set()
        
        # 遍历链表A,将所有节点加入集合
        p = headA
        while p:
            visited.add(p)
            p = p.next
        
        # 遍历链表B,检查节点是否在集合中
        q = headB
        while q:
            if q in visited:
                return q
            q = q.next
        
        return None

解法二:栈方法(从后向前比较)

python

复制代码
class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
        # 使用两个栈存储节点
        stackA, stackB = [], []
        
        # 将链表A的所有节点压栈
        p = headA
        while p:
            stackA.append(p)
            p = p.next
        
        # 将链表B的所有节点压栈
        q = headB
        while q:
            stackB.append(q)
            q = q.next
        
        # 从栈顶开始比较(即从链表尾部开始比较)
        last_common = None
        while stackA and stackB:
            nodeA = stackA.pop()
            nodeB = stackB.pop()
            
            if nodeA != nodeB:
                break
            last_common = nodeA
        
        return last_common

解法三:连接成环法(修改链表结构,不推荐)

python

复制代码
class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
        if not headA or not headB:
            return None
        
        # 找到链表A的尾节点
        tailA = headA
        while tailA.next:
            tailA = tailA.next
        
        # 将链表A的尾节点连接到链表B的头节点,形成环
        tailA.next = headB
        
        # 使用快慢指针检测环的入口(即相交节点)
        slow, fast = headA, headA
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
            
            if slow == fast:
                # 找到环,寻找环的入口
                entry = headA
                while entry != slow:
                    entry = entry.next
                    slow = slow.next
                # 恢复链表结构
                tailA.next = None
                return entry
        
        # 没有环,恢复链表结构
        tailA.next = None
        return None

九、常见问题与解答

Q1: 如果两个链表长度相差很大怎么办?

A1: 双指针法能很好地处理这种情况。无论长度差多大,两个指针最终都会走过相同的总路径长度。

Q2: 为什么不能用值比较来判断相交节点?

A2: 因为链表中可能存在重复的值,但节点是唯一的(通过内存地址区分)。相交节点必须是同一个节点对象,而不只是值相同。

Q3: 如果链表有环怎么办?

A3: 题目假设链表没有环。如果可能有环,需要先判断是否有环,再使用相应的方法。

Q4: 双指针法是否会陷入无限循环?

A4: 不会。因为当两个链表不相交时,两个指针最终会同时到达null,循环结束。

Q5: 如何测试代码的正确性?

A5: 可以测试以下情况:

  1. 两个链表相交于中间节点

  2. 两个链表相交于头节点

  3. 两个链表不相交

  4. 一个链表为空

  5. 两个链表都为空

十、相关题目

1. LeetCode 141. 环形链表

python

复制代码
class Solution:
    def hasCycle(self, head: ListNode) -> bool:
        # 快慢指针法
        if not head or not head.next:
            return False
        
        slow, fast = head, head.next
        
        while slow != fast:
            if not fast or not fast.next:
                return False
            slow = slow.next
            fast = fast.next.next
        
        return True

2. LeetCode 142. 环形链表 II

python

复制代码
class Solution:
    def detectCycle(self, head: ListNode) -> ListNode:
        # 快慢指针找到相遇点
        slow, fast = head, head
        
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
            
            if slow == fast:
                # 找到环,寻找环的入口
                entry = head
                while entry != slow:
                    entry = entry.next
                    slow = slow.next
                return entry
        
        return None

3. LeetCode 206. 反转链表

python

复制代码
class Solution:
    def reverseList(self, head: ListNode) -> ListNode:
        prev, curr = None, head
        
        while curr:
            next_node = curr.next
            curr.next = prev
            prev = curr
            curr = next_node
        
        return prev

十一、总结

核心要点

  1. 双指针法:利用路径长度相等的原理,让两个指针走过相同的总路径

  2. 交换遍历:当一个指针到达链表末尾时,重定向到另一个链表的头节点

  3. 时间复杂度优化:O(m+n)的时间复杂度和O(1)的空间复杂度

算法步骤(双指针法)

  1. 初始化两个指针p和q,分别指向headA和headB

  2. 当p不等于q时循环:

    • 如果p不为空,p移动到下一个节点;否则,p重定向到headB

    • 如果q不为空,q移动到下一个节点;否则,q重定向到headA

  3. 返回p(此时p和q相同,要么是相交节点,要么是None)

时间复杂度与空间复杂度

  • 时间复杂度:O(m+n),最多遍历两个链表各一次

  • 空间复杂度:O(1),只使用常数个指针变量

适用场景

  • 需要找到两个链表的相交节点

  • 空间复杂度要求严格

  • 链表不允许修改

扩展思考

双指针法是解决链表问题的常用技巧,这种思想可以应用于:

  • 环形链表检测

  • 链表中点查找

  • 链表倒数第k个节点

  • 两个链表的合并与比较

掌握相交链表问题的解法不仅能够解决这个特定问题,还能加深对链表操作和双指针技巧的理解,是面试中常见的经典题目。

相关推荐
知无不研9 小时前
选择排序算法
数据结构·算法·排序算法·选择排序
爱学习的阿磊9 小时前
C++中的策略模式应用
开发语言·c++·算法
郝学胜-神的一滴9 小时前
Python中的bisect模块:优雅处理有序序列的艺术
开发语言·数据结构·python·程序人生·算法
筵陌9 小时前
算法:位运算
算法
Remember_9939 小时前
Spring 事务深度解析:实现方式、隔离级别与传播机制全攻略
java·开发语言·数据库·后端·spring·leetcode·oracle
Christo39 小时前
TKDE-2026《Efficient Co-Clustering via Bipartite Graph Factorization》
人工智能·算法·机器学习·数据挖掘
2401_838472519 小时前
C++异常处理最佳实践
开发语言·c++·算法
m0_736919109 小时前
C++中的类型标签分发
开发语言·c++·算法
2301_790300969 小时前
C++与微服务架构
开发语言·c++·算法