漯河市网站建设_网站建设公司_Python_seo优化
2026/1/15 12:23:54 网站建设 项目流程

Java版LeetCode热题100之相交链表:从哈希到双指针的深度解析

本文全面剖析 LeetCode 第160题「相交链表」,涵盖题目理解、多种解法实现、复杂度分析、面试技巧及实际应用场景。无论你是准备面试的新手,还是希望深入理解链表操作的老手,都能从中获得系统性提升。


一、原题回顾

题目编号:LeetCode 160
题目名称:相交链表(Intersection of Two Linked Lists)
难度等级:简单(Easy)但极具启发性

题目描述

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

关键约束

  • 题目数据保证整个链式结构中不存在环
  • 函数返回结果后,链表必须保持其原始结构
  • 相交意味着内存地址相同,而非值相同

示例说明

示例 1:

输入: intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3 输出:Intersected at '8'

图示解释

链表A: 4 → 1 ↘ 8 → 4 → 5 链表B: 5 → 6 → 1 ↗

注意:虽然两个链表都有值为1的节点,但它们是不同的对象(内存地址不同)。只有值为8的节点是同一个对象

示例 2:

输入: intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1 输出:Intersected at '2'

示例 3:

输入: intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2 输出:No intersection

约束条件

  • 1 <= m, n <= 3 * 10⁴(链表长度)
  • 1 <= Node.val <= 10⁵
  • 如果不相交,intersectVal = 0
  • 进阶要求:时间复杂度 O(m + n),空间复杂度 O(1)

二、原题分析

这道题的核心在于理解链表相交的本质

🔑 关键洞察

  1. 相交 ≠ 值相同:必须是同一个内存地址的节点
  2. Y型结构:一旦相交,后续所有节点都共享(形成Y字形)
  3. 无环保证:简化了问题,无需考虑循环检测
  4. 结构保持:不能修改原链表(如不能反转、不能标记)

🎯 解题目标

找到第一个内存地址相同的节点,即两个链表开始共享的部分。

❌ 常见误区

  • 误区1:比较节点值 → 错误!可能有重复值但不同对象
  • 误区2:从后往前找 → 单链表无法反向遍历
  • 误区3:先对齐再比较 → 需要知道长度差,但如何高效获取?

三、答案构思

我们将构建两种主要解法,逐步优化:

✅ 方法一:哈希集合(Hash Set)

  • 思路:存储链表A的所有节点,遍历链表B查找
  • 优点:直观易懂,代码简单
  • 缺点:需要额外 O(m) 空间

✅ 方法二:双指针(Two Pointers)⭐️

  • 思路:利用路径长度相等的巧妙设计
  • 优点:O(1) 空间,满足进阶要求
  • 缺点:思路较难想到,需理解"路径对齐"原理

下面详细展开两种方法。


四、完整答案(Java 实现)

方法一:哈希集合

importjava.util.HashSet;importjava.util.Set;publicclassSolution{publicListNodegetIntersectionNode(ListNodeheadA,ListNodeheadB){// 边界检查if(headA==null||headB==null){returnnull;}// 存储链表A的所有节点Set<ListNode>visited=newHashSet<>();ListNodecurrent=headA;while(current!=null){visited.add(current);current=current.next;}// 遍历链表B,查找第一个在集合中的节点current=headB;while(current!=null){if(visited.contains(current)){returncurrent;// 找到相交节点}current=current.next;}returnnull;// 无相交}}

方法二:双指针(推荐解法)

publicclassSolution{publicListNodegetIntersectionNode(ListNodeheadA,ListNodeheadB){// 边界检查if(headA==null||headB==null){returnnull;}ListNodepA=headA;ListNodepB=headB;// 当两个指针相遇时停止while(pA!=pB){// pA走完自己的路,就走pB的路pA=(pA==null)?headB:pA.next;// pB走完自己的路,就走pA的路pB=(pB==null)?headA:pB.next;}// 如果相交,pA/pB指向相交节点// 如果不相交,pA/pB都为nullreturnpA;}}

💡核心思想

  • 指针A走的总路径:lenA + lenB
  • 指针B走的总路径:lenB + lenA
  • 路径长度相等,必然在相交点或终点相遇!

五、代码分析

方法一:哈希集合

优点

  • 逻辑清晰:先存后查,符合直觉
  • 易于调试:可打印集合内容验证
  • 扩展性强:可轻松修改为查找所有相交点

缺点

  • 空间开销大:最坏情况存储全部m个节点
  • 哈希冲突:虽然Java的HashSet处理良好,但理论上存在

边界处理

  • 空链表检查必不可少
  • 利用Set的contains()方法高效查找

方法二:双指针

优点

  • 空间最优:仅用两个指针变量
  • 代码简洁:核心逻辑仅5行
  • 优雅巧妙:体现算法之美

关键细节

  • 指针重置时机:必须在pA == null时重置,而非pA.next == null
  • 循环终止条件pA != pB,包含相交和不相交两种情况
  • 空指针处理:当不相交时,两指针最终都为null,自然退出

执行过程示例(示例1):

初始: pA=4, pB=5 步骤1: pA=1, pB=6 步骤2: pA=8, pB=1 步骤3: pA=4, pB=8 步骤4: pA=5, pB=4 步骤5: pA=null, pB=5 → pA重置为headB(5) 步骤6: pA=6, pB=null → pB重置为headA(4) 步骤7: pA=1, pB=1 步骤8: pA=8, pB=8 → 相遇!返回8

六、时间复杂度和空间复杂度分析

方法时间复杂度空间复杂度说明
哈希集合O(m + n)O(m)遍历A存入,遍历B查找
双指针O(m + n)O(1)每个指针最多走 m+n 步

详细推导(双指针)

相交情况

  • 设链表A独有部分长度为a,链表B独有部分长度为b,公共部分长度为c
  • 指针A路径:a + c + b
  • 指针B路径:b + c + a
  • 总步数:a + b + c ≤ m + n

不相交情况

  • 指针A路径:m + n
  • 指针B路径:n + m
  • 总步数:m + n

📊性能对比(m=n=30000)

  • 哈希集合:60000次操作 + 30000个对象存储
  • 双指针:60000次操作 + 2个指针变量

在内存受限环境(如嵌入式系统),双指针优势明显!


七、问题解答(FAQ)

Q1:为什么双指针方法能保证找到相交点?

:因为两个指针走的总路径长度相等

  • 如果相交:都在第(a+b+c)步到达相交点
  • 如果不相交:都在第(m+n)步到达null

这种"路径对齐"确保了必然相遇。

Q2:能否先计算长度差,然后对齐再比较?

:完全可以!这是另一种O(1)空间的解法:

publicListNodegetIntersectionNode(ListNodeheadA,ListNodeheadB){intlenA=getLength(headA),lenB=getLength(headB);// 让长链表先走差值步if(lenA>lenB){for(inti=0;i<lenA-lenB;i++){headA=headA.next;}}else{for(inti=0;i<lenB-lenA;i++){headB=headB.next;}}// 同步遍历while(headA!=null&&headB!=null){if(headA==headB)returnheadA;headA=headA.next;headB=headB.next;}returnnull;}privateintgetLength(ListNodehead){intlength=0;while(head!=null){length++;head=head.next;}returnlength;}

优缺点对比

  • 优点:逻辑更直观,容易理解
  • 缺点:需要三次遍历(两次算长度,一次找交点),而双指针只需一次

Q3:如果链表有环,还能用这些方法吗?

:不能直接使用!需要先检测环,再分类讨论:

  1. 两链表都无环 → 本题情况
  2. 一个有环,一个无环 → 不可能相交
  3. 两链表都有环 → 相交点可能在环内或环外

这会大大增加复杂度,属于Hard级别问题。

Q4:为什么题目强调"保持原始结构"?

:防止作弊方案!比如:

  • 修改节点值做标记
  • 反转链表再比较
  • 添加临时指针字段

这些都会破坏原始数据,在实际系统中不可接受。


八、优化思路

1. 早期终止优化(哈希法)

在存储链表A时,如果发现某个节点的next为null且不在链表B的预期范围内,可提前终止?
:不行!因为我们不知道链表B的结构,无法预判。

2. 双指针的变种实现

有人担心指针重置会导致无限循环,其实不会:

  • 每个指针最多重置一次
  • 总步数严格限制在 m+n

3. 并行化思考

理论上可以并行遍历两个链表,但在单线程环境下无意义,且同步开销大。

结论:双指针已是理论最优解,无需进一步优化。


九、数据结构与算法基础知识点回顾

1. 单链表基础

  • 节点结构val+next指针
  • 遍历操作:从头到尾,O(n)时间
  • 空间特性:动态分配,非连续内存

2. 哈希表(HashSet)

  • 底层实现:数组 + 链表/红黑树(Java 8+)
  • 时间复杂度:平均O(1)插入/查找
  • 空间开销:需要存储所有元素

3. 双指针技巧

  • 同向双指针:快慢指针(如环检测)
  • 相向双指针:从两端向中间(如两数之和)
  • 交叉双指针:本题的路径交叉技巧

4. 复杂度分析要点

  • 时间复杂度:关注最坏情况下的操作次数
  • 空间复杂度:只计算额外使用的空间,不包括输入
  • 进阶要求:O(1)空间通常意味着不能使用集合、栈、递归等

5. 内存地址 vs 值比较

  • == 比较:引用相等(内存地址相同)
  • equals() 比较:值相等(可重写)
  • 本题要求:必须用==比较节点引用

十、面试官提问环节

❓ 面试官:为什么不用递归解决这个问题?

考察点:对空间复杂度的理解

回答
递归会使用系统栈,空间复杂度为O(max(m,n)),不满足O(1)的要求。而且递归无法直接解决路径长度不同的问题,仍需要额外的长度计算。迭代方法更符合题目约束。


❓ 面试官:如果允许修改链表结构,有什么更简单的办法?

考察点:创造性思维

回答
可以采用"标记法":

  1. 遍历链表A,将每个节点的值设为特殊标记(如负数)
  2. 遍历链表B,找到第一个被标记的节点
  3. 恢复链表A的原始值

但这种方法:

  • 破坏了"保持原始结构"的要求
  • 假设节点值可修改且有可用的标记值
  • 在多线程环境下不安全

所以实际开发中不推荐。


❓ 面试官:你的双指针解法中,如果两个链表完全相同(不仅是相交),会怎样?

考察点:边界情况考虑

回答
如果两个链表完全相同(即headA == headB),那么在第一次循环时就会发现pA == pB,直接返回headA。这是正确的,因为相交点就是头节点。

实际上,这种情况是相交的特例(独有部分长度为0)。


❓ 面试官:能否用这个思路解决"找链表中间节点"的问题?

考察点:知识迁移能力

回答
不能直接应用。找中间节点通常用快慢指针(快指针走两步,慢指针走一步),这是另一种双指针技巧。本题的"路径交叉"思想适用于两个独立序列的对齐问题,而快慢指针适用于单个序列的内部关系检测

不过,两者都体现了"通过指针移动策略解决遍历问题"的核心思想。


十一、这道算法题在实际开发中的应用

1. Git版本控制系统

  • 分支合并:不同分支的历史提交形成链表结构
  • 共同祖先查找:类似找相交点,确定合并基准
  • 实际实现:Git使用更复杂的图算法,但基本思想相似

2. 内存泄漏检测

  • 对象引用链:追踪对象的引用路径
  • 共享对象识别:多个引用链指向同一对象时的检测
  • 工具实现:如Java的VisualVM使用类似技术

3. 数据库事务日志

  • WAL(Write-Ahead Logging):事务日志形成链表
  • 检查点恢复:多个日志流在某个点合并
  • 故障恢复:需要找到最近的共同检查点

4. 分布式系统一致性

  • Raft协议:日志复制形成链表结构
  • Leader选举:需要找到多数派的日志交集
  • 状态同步:确定从哪个日志条目开始同步

5. 编译器优化

  • 控制流图(CFG):基本块形成图结构
  • 支配节点分析:找多个路径的共同祖先
  • 死代码消除:识别不可达的代码路径

💡核心价值:在复杂的数据依赖关系中,快速定位共同的交汇点,这对于系统的一致性、恢复性和优化至关重要。


十二、相关题目推荐

题号题目关联点
160. 相交链表本题链表相交检测
141. 环形链表快慢指针检测环双指针基础
142. 环形链表 II找环入口快慢指针进阶
206. 反转链表链表基本操作指针操作练习
234. 回文链表链表对称性双指针应用
19. 删除链表的倒数第N个节点双指针间隔距离控制

🔗进阶挑战:尝试解决[142. 环形链表 II],它结合了环检测和相交点查找的思想。


十三、总结与延伸

核心收获

  1. 引用相等 vs 值相等:在对象比较中要特别注意
  2. 路径对齐思想:通过巧妙的路径设计解决不对称问题
  3. 空间复杂度意识:O(1)空间往往需要更精巧的算法设计
  4. 链表操作基础:指针移动是链表问题的核心技能

延伸思考

  • 多链表相交:如果有k个链表,如何找共同相交点?
    • 可以两两合并,时间复杂度O(k(m+n))
    • 或使用最小堆维护k个指针
  • 动态相交检测:如果链表在运行时动态变化?
    • 需要维护额外的元数据或使用观察者模式
  • 概率算法:能否用布隆过滤器近似解决?
    • 可以,但有误判率,不适合精确场景

最终建议

  • 面试首选双指针解法:简洁、高效、体现算法思维
  • 务必理解原理:不要死记硬背,要能解释为什么有效
  • 手写无bug:注意空指针检查和循环终止条件
  • 对比多种解法:展示你的算法设计能力

结语:相交链表看似简单,却蕴含着深刻的算法思想。从哈希的直观到双指针的巧妙,每一步都是对问题本质的深入理解。掌握它,你不仅解决了一道题,更获得了解决"路径对齐"类问题的通用思维框架!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询