【LeetCodeHOT100】 160. 相交链表 —— Java多解法详解

题目链接160. 相交链表 - 力扣(LeetCode)


一、题目描述

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

题目数据保证整个链式结构中不存在环。

注意 :函数返回结果后,链表必须保持其原始结构

【关键理解】 :何为相交?

相交指的是节点为同一个节点 ,即两个指针指向内存中的同一个对象,而不是两个节点值相等。这一点非常重要------不能通过比较 val 来判断相交,必须用 == 比较节点引用(即内存地址)。


二、方法概览

方法 时间复杂度 空间复杂度 难度 特点
暴力法 O(m×n) O(1) 最直观,双重循环
哈希表法 O(m+n) O(m) 或 O(n) ⭐⭐ 空间换时间
栈解法 O(m+n) O(m+n) ⭐⭐ 后进先出特性
长度差双指针 O(m+n) O(1) ⭐⭐⭐ 消除长度差
互换遍历双指针 O(m+n) O(1) ⭐⭐⭐ 最优雅,代码最短

注:mn 分别为链表 headAheadB 的长度。


三、准备工作:链表节点定义

所有解法均基于以下 ListNode 定义(题目已给出):

java

复制代码
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */

四、解法一:暴力法(双重循环)

💡 思路

最直接的想法:遍历链表 A 中的每个节点,对于每个节点,再遍历链表 B 中的所有节点,看是否有相同的节点(内存地址相同)。第一个找到的相同节点就是交点。

📝 代码实现

java

复制代码
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) {
            return null;
        }
        
        ListNode curA = headA;
        while (curA != null) {
            ListNode curB = headB;
            while (curB != null) {
                if (curA == curB) {
                    return curA;
                }
                curB = curB.next;
            }
            curA = curA.next;
        }
        return null;
    }
}
⏱ 复杂度分析
  • 时间复杂度 :O(m × n),最坏情况下需要遍历 m × n 次。

  • 空间复杂度:O(1),仅使用常数个额外变量。

📌 点评

暴力法是最容易想到的解法,但效率较低。在链表长度较大时(m, n ≤ 3×10⁴)会超时,仅适合理解题意或小规模数据。


五、解法二:哈希表法

💡 思路

利用哈希集合(HashSet)以空间换时间。先用哈希集合存储链表 A 的所有节点,再遍历链表 B,第一个在哈希集合中存在的节点就是交点。由于哈希集合查找是 O(1) 的,整体时间复杂度可以降到 O(m+n)。

📝 代码实现

java

复制代码
import java.util.HashSet;
import java.util.Set;

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) {
            return null;
        }
        
        Set<ListNode> visited = new HashSet<>();
        
        // 遍历链表 A,将所有节点加入哈希集合
        ListNode curA = headA;
        while (curA != null) {
            visited.add(curA);
            curA = curA.next;
        }
        
        // 遍历链表 B,第一个在集合中的节点就是交点
        ListNode curB = headB;
        while (curB != null) {
            if (visited.contains(curB)) {
                return curB;
            }
            curB = curB.next;
        }
        
        return null;
    }
}
⏱ 复杂度分析
  • 时间复杂度:O(m + n),需要遍历两个链表各一次。

  • 空间复杂度:O(m) 或 O(n),需要存储其中一个链表的所有节点。

📌 点评

哈希表法代码简洁,思路清晰,是面试中常用的解法。缺点是需要额外的 O(m) 或 O(n) 空间。题目中的进阶要求是 O(1) 空间,所以这并非最优解。


六、解法三:栈解法

💡 思路

利用栈"后进先出"的特性。将两个链表的所有节点分别压入两个栈中,然后同时弹出栈顶元素进行比较。由于相交节点之后的节点全部相同,所以从栈顶开始弹出时,最后一组相等的节点之后的下一个节点(即最后一组相等节点的后一个)就是第一个相交的节点。

示意图

  • 链表 A:a1 → a2 → c1 → c2 → c3

  • 链表 B:b1 → b2 → b3 → c1 → c2 → c3

  • 压栈后,栈顶都是 c3,依次弹出:c3c2 相等,c1 相等,弹出 a2b3 时不再相等。则最后一组相等节点的后一个节点是 c1(即第一个相交节点)。

📝 代码实现

java

复制代码
import java.util.ArrayDeque;
import java.util.Deque;

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) {
            return null;
        }
        
        Deque<ListNode> stackA = new ArrayDeque<>();
        Deque<ListNode> stackB = new ArrayDeque<>();
        
        // 将链表 A 的所有节点压入栈 A
        ListNode curA = headA;
        while (curA != null) {
            stackA.push(curA);
            curA = curA.next;
        }
        
        // 将链表 B 的所有节点压入栈 B
        ListNode curB = headB;
        while (curB != null) {
            stackB.push(curB);
            curB = curB.next;
        }
        
        ListNode intersection = null;
        // 同时弹出栈顶元素,直到遇到不相等的节点
        while (!stackA.isEmpty() && !stackB.isEmpty()) {
            if (stackA.peek() == stackB.peek()) {
                intersection = stackA.pop();
                stackB.pop();
            } else {
                break;
            }
        }
        
        return intersection;
    }
}
⏱ 复杂度分析
  • 时间复杂度:O(m + n),需要遍历两个链表各两次(一次入栈,一次出栈)。

  • 空间复杂度:O(m + n),需要两个栈存储两个链表的所有节点。

📌 点评

栈解法利用了相交链表"尾部相同"的特性,思路巧妙。但空间复杂度较高,且代码稍显冗长。在面试中可作为备选方案展示思考的多样性。


七、解法四:长度差双指针法

💡 思路

如果两个链表相交,那么相交节点之后的部分长度是相同的。因此,核心思路是:让两个链表从"距离尾部相同距离"的位置开始同时向前遍历

具体步骤:

  1. 分别计算两个链表的长度 lenAlenB

  2. 计算长度差 gap = |lenA - lenB|

  3. 让较长的链表先走 gap 步,此时两个指针距离尾部的距离相等。

  4. 两个指针同步向前移动,第一次相遇的节点就是交点。

📝 代码实现

java

复制代码
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) {
            return null;
        }
        
        // 1. 计算两个链表的长度
        ListNode curA = headA;
        ListNode curB = headB;
        int lenA = 0, lenB = 0;
        
        while (curA != null) {
            lenA++;
            curA = curA.next;
        }
        while (curB != null) {
            lenB++;
            curB = curB.next;
        }
        
        // 2. 重新指向头节点
        curA = headA;
        curB = headB;
        
        // 3. 让较长的链表先走差值步
        int gap = Math.abs(lenA - lenB);
        if (lenA > lenB) {
            while (gap-- > 0) {
                curA = curA.next;
            }
        } else {
            while (gap-- > 0) {
                curB = curB.next;
            }
        }
        
        // 4. 同步向前遍历,寻找交点
        while (curA != null && curB != null) {
            if (curA == curB) {
                return curA;
            }
            curA = curA.next;
            curB = curB.next;
        }
        
        return null;
    }
}
⏱ 复杂度分析
  • 时间复杂度:O(m + n),需要遍历两个链表各两次。

  • 空间复杂度:O(1),仅使用常数个额外变量。

📌 点评

长度差双指针法满足题目进阶要求的 O(1) 空间,思路清晰易懂。需要两次遍历,代码稍长但逻辑明确,是面试中值得推荐的解法之一。


八、解法五:互换遍历双指针法(最优解)

💡 思路

这是本题最优雅、最巧妙的解法,代码极短且无需额外空间。

核心思想 :创建两个指针 pApB,分别指向 headAheadB,然后同时向前移动 。当一个指针到达链表末尾时,将其重定向到另一个链表的头节点,继续移动。如果两个链表相交,两个指针最终会在相交节点相遇;如果不相交,它们会同时变为 null

数学原理

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

  • 指针 pA 走过的总路程为 a + c + b,指针 pB 走过的总路程为 b + c + a

  • 两者相等,因此 pApB 会同时到达交点(如果相交)或同时到达 null(如果不相交)。

示意图(链表示例):

text

复制代码
链表 A:4 → 1 → 8 → 4 → 5
          ↗
链表 B:5 → 0 → 1
(注:实际相交节点是内存地址相同的节点,此处简化示意)

pA 路径:4 → 1 → 8 → 4 → 5 → null → 5 → 0 → 1 → 8(在 8 处与 pB 相遇)
pB 路径:5 → 0 → 1 → 8 → 4 → 5 → null → 4 → 1 → 8(在 8 处与 pA 相遇)
📝 代码实现

java

复制代码
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) {
            return null;
        }
        
        ListNode pA = headA;
        ListNode pB = headB;
        
        // 当 pA == pB 时退出循环
        // 如果相交,pA 和 pB 会在交点相遇
        // 如果不相交,pA 和 pB 会同时变为 null
        while (pA != pB) {
            pA = (pA == null) ? headB : pA.next;
            pB = (pB == null) ? headA : pB.next;
        }
        
        return pA;
    }
}
⏱ 复杂度分析
  • 时间复杂度:O(m + n),每个指针最多遍历两个链表各一次。

  • 空间复杂度:O(1),仅使用两个指针变量。

📌 点评

这是本题的最优解,也是面试官最希望看到的答案。 代码仅需寥寥数行,同时满足时间复杂度 O(m+n) 和空间复杂度 O(1) 的双重要求。核心在于巧妙利用"路程相等"的数学原理,通过互换遍历消除了长度差的影响。


九、解法对比总结

解法 代码量 空间复杂度 是否满足进阶要求 适用场景
暴力法 O(1) 小规模数据或理解题意
哈希表法 O(m) 或 O(n) 优先考虑时间效率,不限制空间
栈解法 O(m+n) 展示多种思路
长度差法 中长 O(1) 空间要求严格,逻辑清晰
互换遍历法 极短 O(1) 面试推荐,最优解

十、注意事项与常见坑点

  1. 比较的是节点引用,而非节点值

    这是本题最容易踩的坑!不能通过比较 val 来判断相交,因为不同节点可能存储相同的值。必须使用 == 比较节点对象本身(即内存地址)。

  2. 不能修改链表结构

    题目明确要求"函数返回结果后,链表必须保持其原始结构"。这意味着不能修改任何节点的 next 指针或 val 值。

  3. 空链表处理

    如果任一链表为空,直接返回 null

  4. 链表无环

    题目已保证链表结构中不存在环,无需额外处理。


十一、官方题解

相关推荐
曹牧2 小时前
Java:将XML字符串上传到FTP服务器
java·开发语言
存在的五月雨2 小时前
Mqtt发送信息报错
java
杨凯凡2 小时前
【016】集合框架总览:List/Set/Map 与线程安全
java·数据结构·list
Predestination王瀞潞2 小时前
Java EE3-我独自整合(第六章:Spring AOP 工作流程与切入点表达式)
java·spring·java-ee
景庆1972 小时前
vscode启动springBoot项目配置,激活环境
java·开发语言·vscode
小则又沐风a2 小时前
Linux使用指南和基础指令(1)
java·linux·运维
自我意识的多元宇宙2 小时前
二叉树的遍历和线索二叉树--先序二叉树和后续二叉树
数据结构
im_AMBER2 小时前
Leetcode 159 无重复字符的最长子串 | 长度最小的子数组
javascript·数据结构·学习·算法·leetcode
三千星2 小时前
Java开发者转型AI工程化Week 2:从核心能力到生产就绪
java·ai编程