algorithm-base 算法图解:剑指 Offer 52 与 LeetCode 160 两个链表的第一个公共节点(相交链表)双指针与哈希解法全解析
文档教程知识库【免费下载链接】algorithm-base一位酷爱做饭的程序员立志用动画将算法说的通俗易懂。我的面试网站 www.chengxuchu.com项目地址https://gitcode.com/gh_mirrors/al/algorithm-base点击查看免费下载本篇基于 algorithm-base 仓库中剑指Offer52两个链表的第一个公共节点一文的完整内容整理扩充而成。本文将以相交链表这一经典面试题为载体系统讲解 HashSet 存储法与双指针交替遍历法两种主流解法并给出 Java、C、JavaScript、Python、Swift、Go 六种语言的完整可运行代码帮助你理解链表按节点对象身份比较的核心语义掌握空间 O(1) 时间 O(n) 的优雅解法。题目背景与考点本题在算法题源中对应两个编号剑指 Offer 52「两个链表的第一个公共节点」与LeetCode 160「相交链表Intersection of Two Linked Lists」二者为同一道题是剑指 Offer 系列中的经典题目也是链表板块收尾阶段的必刷题。在 algorithm-base 仓库中本题被收录在两个分类之下链表篇作为链表专题的收官题目README.md 的双指针分类与 leetcode141环形链表、leetcode328奇偶链表 等共同构成双指针解题范式专题。刷本题前建议先掌握两类前置知识链表基础结构单链表由数据域与指针域组成最后一个节点指向 null。可阅读仓库中的链表详解补全概念ListNode 与 HashSet 的 APIJava 中创建节点使用new ListNode(0)HashSet 是不允许有重复元素的集合但允许 null 值、无序、非线程安全的容器其常用方法add()、contains()的具体说明见仓库的Leetcode常用类和函数。题目描述输入两个链表找出它们的第一个公共节点。例如下图所示的两条链表从某个节点开始两条链表合并为一条后续节点完全共用我们的任务就是返回这个第一个相交的节点即图中黄色节点。理解这道题的关键在于链表相交是按节点对象内存地址/引用相交而不是按节点存储的值相等。也就是说即使两个节点的val完全相同只要不是同一个节点对象就不算相交。因此下面的两种主流解法比较的都是节点引用本身而非节点值。方法一HashSet 存储法算法思路先遍历链表 A将遍历到的每一个节点对象存入 HashSet再遍历链表 B每遍历一个节点就检查其是否已存在于 HashSet 中若某个节点已存在说明它就是两条链表的第一个公共节点直接返回若遍历完链表 B 仍无命中则两条链表不相交返回 null此时tempb已走到链表末尾。public class Solution { public ListNode getIntersectionNode (ListNode headA, ListNode headB) { ListNode tempa headA; ListNode tempb headB; //定义Hashset HashSetListNode arr new HashSetListNode(); //遍历链表A将所有值都存到arr中 while (tempa ! null) { arr.add(tempa); tempa tempa.next; } //遍历列表B如果发现某个结点已在arr中则直接返回该节点 while (tempb ! null) { if (arr.contains(tempb)) { return tempb; } tempb tempb.next; } //若上方没有返回此刻tempb为null return tempb; } }class Solution { public: ListNode * getIntersectionNode(ListNode *headA, ListNode *headB) { ListNode * tempa headA; ListNode * tempb headB; //定义Hashset set ListNode * arr; //遍历链表A将所有值都存到arr中 while (tempa ! nullptr) { arr.insert(tempa); tempa tempa-next; } //遍历列表B如果发现某个结点已在arr中则直接返回该节点 while (tempb ! nullptr) { if (arr.find(tempb) ! arr.end()) { return tempb; } tempb tempb-next; } //若上方没有返回此刻tempb为null return tempb; } };var getIntersectionNode function (headA, headB) { let tempa headA; let tempb headB; //定义Hashset let arr new Set(); //遍历链表A将所有值都存到arr中 while (tempa) { arr.add(tempa); tempa tempa.next; } //遍历列表B如果发现某个结点已在arr中则直接返回该节点 while (tempb) { if (arr.has(tempb)) { return tempb; } tempb tempb.next; } //若上方没有返回此刻tempb为null return tempb; };class Solution: def getIntersectionNode(self, headA: ListNode, headB: ListNode) - ListNode: tempa headA tempb headB # 定义Hashset arr set() # 遍历链表A将所有值都存到arr中 while tempa is not None: arr.add(tempa) tempa tempa.next # 遍历列表B如果发现某个结点已在arr中则直接返回该节点 while tempb is not None: if tempb in arr: return tempb tempb tempb.next # 若上方没有返回此刻tempb为null return tempbclass Solution { func getIntersectionNode(_ headA: ListNode?, _ headB: ListNode?) - ListNode? { var tempa headA var tempb headB var arr:SetListNode [] //遍历链表A将所有值都存到arr中 while tempa ! nil { arr.insert(tempa!) tempa tempa?.next } //遍历列表B如果发现某个结点已在arr中则直接返回该节点 while tempb ! nil { if arr.contains(tempb!) { return tempb } tempb tempb?.next } //若上方没有返回此刻tempb为null return tempb } } extension ListNode: Hashable, Equatable { public func hash(into hasher: inout Hasher) { hasher.combine(val) hasher.combine(ObjectIdentifier(self)) } public static func (lhs: ListNode, rhs: ListNode) - Bool { return lhs rhs } }实现细节说明Swift 需要额外扩展由于 Swift 的Set要求元素遵循Hashable与Equatable协议原文档的 Swift 版本通过extension ListNode补全了这两个协议其中hash(into:)混合了val与对象唯一标识ObjectIdentifier使用按引用判等——这再次印证了按节点对象比较的核心语义C 使用setListNode*存放的是指针比较的也是指针地址JavaScript/Python 天然支持对象入集Set与set()对引用类型默认按对象身份去重代码最简洁。复杂度分析指标数值说明时间复杂度O(m n)分别遍历两条链表各一次m、n 为两链表长度空间复杂度O(m)需要额外存储链表 A 的全部节点该解法思路直白、正确性显而易见代价是空间开销较大。仓库的Leetcode常用类和函数中对该容器的补充说明也适用于本题HashSet 基于 HashMap 实现不允许重复元素无序且非线程安全。方法二双指针交替遍历法最优解算法思路与方法一借助外部容器不同双指针法只需两个指针即可在 O(1) 空间内解决问题思路如下定义指针tempa从headA出发指针tempb从headB出发两个指针同步前进每次移动一步当某个指针走到链表末尾null时掉头去另一条链表的头部继续遍历因为两个指针移动速度相同、走过的总路程相同它们必然会在某个时刻指向同一个节点——这个节点就是第一个公共节点若两条链表不相交两个指针最终会同时走到 null循环退出返回 null。直观理解tempa走过的路程为链表 A 全长 链表 B 公共部分之前的长度tempb走过的路程为链表 B 全长 链表 A 公共部分之前的长度二者相等因此它们在公共区域的起点必然相遇。public class Solution { public ListNode getIntersectionNode (ListNode headA, ListNode headB) { //定义两个节点 ListNode tempa headA; ListNode tempb headB; //循环 while (tempa ! tempb) { //如果不为空就指针下移为空就跳到另一链表的头部 tempa tempa ! null ? tempa.next: headB; tempb tempb ! null ? tempb.next: headA; } return tempa;//返回tempb也行 } }class Solution { public: ListNode * getIntersectionNode(ListNode *headA, ListNode *headB) { //定义两个节点 ListNode * tempa headA; ListNode * tempb headB; //循环 while (tempa ! tempb) { //如果不为空就指针下移为空就跳到另一链表的头部 tempa tempa ! nullptr ? tempa-next: headB; tempb tempb ! nullptr ? tempb-next: headA; } return tempa;//返回tempb也行 } };var getIntersectionNode function (headA, headB) { //定义两个节点 let tempa headA; let tempb headB; //循环 while (tempa ! tempb) { //如果不为空就指针下移为空就跳到另一链表的头部 tempa tempa ! null ? tempa.next : headB; tempb tempb ! null ? tempb.next : headA; } return tempa; //返回tempb也行 };class Solution: def getIntersectionNode(self, headA: ListNode, headB: ListNode) - ListNode: # 定义两个节点 tempa headA tempb headB # 循环 while tempa is not tempb: # 如果不为空就指针下移为空就跳到另一链表的头部 tempa tempa.next if tempa is not None else headB tempb tempb.next if tempb is not None else headA return tempa # 返回tempb也行class Solution { func getIntersectionNode(_ headA: ListNode?, _ headB: ListNode?) - ListNode? { //定义两个节点 var tempa headA var tempb headB //循环 while tempa ! tempb { // 如果不为空就指针下移为空就跳到另一链表的头部 tempa tempa ! nil ? tempa?.next : headB tempb tempb ! nil ? tempb?.next : headA } return tempa //返回tempb也行 } }func getIntersectionNode(headA, headB *ListNode) *ListNode { tempA, tempB : headA, headB for tempA ! tempB { // 如果不为空就指针下移为空就跳到另一链表的头部 if tempA nil { tempA headB } else { tempA tempA.Next } if tempB nil { tempB headA } else { tempB tempB.Next } } return tempA }边界情况分析相交于链表头headA headB时循环条件一开始就不成立直接返回头节点正确不相交假设链表 A 长 m、链表 B 长 n两指针各走 m n 步后同时为 nulltempa tempb成立循环退出返回 null正确一个链表为空空链表指针立即为 null另一指针走完自身链表后也为 null返回 null正确。复杂度分析指标数值说明时间复杂度O(m n)每个指针最多走 m n 步空间复杂度O(1)仅使用两个指针无额外容器这是本题的最优解也是面试中最受青睐的写法思想巧妙但代码极短六种语言的核心逻辑均只有三五行。与快慢指针的关联本题的双指针属于相遇型双指针与仓库中另一道经典题leetcode141环形链表快慢指针判断环同属双指针范式环形链表利用速度差追及本题利用路程对齐相交二者共同点是通过指针的相对运动消除链表长度差异带来的干扰。方法三拓展长度差法原文档的贡献者 jaredliw 补充了另外两种值得一试的解法此处完整保留并展开说明。思路先分别遍历两条链表统计长度。设较长链表比短链表长 k 个节点则让较长链表的指针先走 k 步之后两个指针再同步前进。由于此时两个指针距离公共节点的剩余路程一致它们必然同时到达第一个公共节点。原理链表相交后公共部分对两条链表是完全共享的因此两链表尾部对齐后公共节点到链表末尾的距离相等。长度差法通过先走 k 步显式完成对齐与双指针法的掉头隐式对齐殊途同归。方法四拓展成环法思路将其中一条链表的头尾相连把链表 A 的尾节点 next 指向链表 A 的头节点形成环此时问题转化为在一条带环链表中寻找环的入口节点——而这个环的入口恰好就是两链表的第一个公共节点。直接套用仓库中leetcode142环形链表2讲解的快慢指针找环入口算法即可求解。注意该解法会修改原链表结构实际工程使用后需要恢复链表否则会破坏输入数据但它把相交问题统一到了成环问题的解题框架下从模型归约的角度看非常巧妙正如贡献者所说拍腿叫好。四种解法对比总结方法时间复杂度空间复杂度是否修改链表特点HashSet 存储法O(m n)O(m)否思路最直观适合快速 AC双指针交替遍历法O(m n)O(1)否最优解代码极简面试首选长度差法O(m n)O(1)否显式对齐长度易于推导证明成环法O(m n)O(1)是需恢复模型归约巧妙与环形链表题打通仓库内延伸阅读剑指Offer52两个链表的第一个公共节点本文原文档链表详解链表基础概念与类型Leetcode常用类和函数ListNode、HashSet、Set 的 API 速查leetcode141环形链表快慢指针判断环leetcode142环形链表2快慢指针找环入口成环法前置知识README.md查看链表篇与双指针专题的完整题目索引小结本题作为链表板块的收官题核心考点在于节点按引用比较的语义理解以及用双指针把空间复杂度降到 O(1) 的经典技巧。掌握 HashSet 法保证正确性吃透双指针法赢得复杂度优势再辅以长度差法与成环法的思路拓展即可从容应对面试中的变体提问。赞分享文档教程知识库【免费下载链接】algorithm-base一位酷爱做饭的程序员立志用动画将算法说的通俗易懂。我的面试网站 www.chengxuchu.com项目地址https://gitcode.com/gh_mirrors/al/algorithm-base点击查看免费下载相关推荐LeetCode-Book 剑指 Offer 52 详解双指针对齐法求两个链表的第一个公共节点LeetCode Book 剑指 Offer 52 详解双指针对齐法求两个链表的第一个公共节点 本篇基于 LeetCode Book 仓库中《剑指 Offer示例工程CS-Notes 剑指 Offer 题解 52用 O(1) 空间的双指针法求两个链表的第一个公共结点CS Notes 剑指 Offer 题解 52用 O 1 空间的双指针法求两个链表的第一个公共结点 本篇基于 CS Notes 仓库中剑指 Offer 题解的知识库文档教程LeetCode 160. 相交链表Intersection of Two Linked Lists题解哈希法与双指针法详解LeetCode 160. 相交链表Intersection of Two Linked Lists题解哈希法与双指针法详解 导读 本文基于开源仓库 le文档教程知识库上一篇uBlock Origin终极指南3步打造纯净无广告的浏览体验下一篇Torrentio Scraper如何打造你的专属影视资源聚合引擎创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

C++ SFINAE从入门到放弃?不,C++20 Concepts让你10分钟学会模板约束

C++ SFINAE从入门到放弃?不,C++20 Concepts让你10分钟学会模板约束

C SFINAE从入门到放弃?不,C20 Concepts让你10分钟学会模板约束 摘要:本文用“同一个需求,三种写法”的方式,讲透C模板约束的演进——从SFINAE的“替换失败并非错误”原理,到std::enable_if的实战用法&#…

2026/9/24 16:15:22 阅读更多 →
Skia 官方文档站 Markdown 写作指南:基于 Hugo 与 Docsy 的内容组织、Frontmatter 与渲染特性详解

Skia 官方文档站 Markdown 写作指南:基于 Hugo 与 Docsy 的内容组织、Frontmatter 与渲染特性详解

图形学 【免费下载链接】skia Skia is a complete 2D graphic library for drawing Text, Geometries, and Images. See documentation for contribution instructions. 项目地址: https://gitcode.com/gh_mirrors/ski/skia 点击查看 免费下载 导读 本文是 Skia 官…

2026/9/24 16:15:22 阅读更多 →
MemOS 消息查询 API 实战:使用 POST /product/get/message 拉取原始对话历史

MemOS 消息查询 API 实战:使用 POST /product/get/message 拉取原始对话历史

人工智能大模型Agent 记忆AI AgentRAG知识图谱dsh-plugin 【免费下载链接】MemOS Self-evolving memory OS for LLM & AI Agents: ultra-persistent memory, hybrid-retrieval, and cross-task skill reuse, with 35.24% token savings and DeepSeek Harness support. 项目…

2026/9/24 16:15:22 阅读更多 →

最新新闻

Linux 上部署 Ollama 本地大模型:从零安装到模型选型与加速实践

Linux 上部署 Ollama 本地大模型:从零安装到模型选型与加速实践

我先说明一下这篇要写什么:Ollama 是目前在 Linux 上本地跑大语言模型最顺手的工具,没有之一。它的安装、模型拉取、API 调用、服务管理,全部集中在一个命令行工具里,对刚接触本地大模型的人来说,几乎是门槛最低的一条…

2026/9/24 19:13:48 阅读更多 →
SpringBoot+Vue乡村政务办公系统:从源码到部署全流程解析

SpringBoot+Vue乡村政务办公系统:从源码到部署全流程解析

拿这个SpringBootVue 乡村政务办公系统平台的项目源码当毕设或者练手项目,说实话是挺聪明的选择。前后端分离是目前 Java Web 岗位的主流工作模式,技术栈又是 SpringBoot Vue 这种面试常聊的组合,而且题目里带了完整的 SQL 脚本和接口文档&a…

2026/9/24 19:13:47 阅读更多 →
CompletableFuture 超时重试并行流实战:线程池调优与批量任务兜底方案

CompletableFuture 超时重试并行流实战:线程池调优与批量任务兜底方案

做 Java 后端开发,只要跟外部接口打过交道,就绕不开三个词:超时、重试、并行。尤其用 CompletableFuture 做异步编排之后,很多同事容易把它当成一个“更高级的线程池工具”,结果线上一压测就出现线程堆积、任务卡死、批…

2026/9/24 19:13:47 阅读更多 →
OpenHarmony+Flutter跨端状态管理:MobX四层契约实践

OpenHarmony+Flutter跨端状态管理:MobX四层契约实践

1. 为什么要在OpenHarmony上跑Flutter?这不是“技术炫技”,而是真实产线里的生存策略我第一次在LiteOS-M设备上把Flutter UI渲染出来时,手边正摆着三台样机:一台是客户指定的OpenHarmony 3.2 LTS轻量系统设备(主控为Co…

2026/9/24 19:13:47 阅读更多 →
AI智能体协同编程实战:Qoder使用经验与高效协作技巧

AI智能体协同编程实战:Qoder使用经验与高效协作技巧

程序员圈子里最近讨论得比较多的,是阿里巴巴出的这个Qoder,定位是AI智能体协同编程工具。我把它装进IDE用了大概三周,从最开始只会让它补全函数,到后面让它独立跨文件改代码、做代码审查、处理异常日志,中间踩了不少坑…

2026/9/24 19:13:47 阅读更多 →
PaddleNLP tie_weights 权重绑定能力设计与实现全解析(RFC No.103)

PaddleNLP tie_weights 权重绑定能力设计与实现全解析(RFC No.103)

PaddleNLP tie_weights 权重绑定能力设计与实现全解析(RFC No.103) 【免费下载链接】PaddleNLP Easy-to-use and powerful LLM and SLM library with awesome model zoo. 项目地址: https://gitcode.com/gh_mirrors/pa/PaddleNLP 导读 权重绑定&…

2026/9/24 19:12:45 阅读更多 →

日新闻

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为…

2026/9/24 0:00:19 阅读更多 →
单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

简介:一份基于单细胞RNA测序数据的细胞类型注释算法研究Python毕业设计源码,针对计算机相关专业正在做毕设或需要项目实战的学习者,可用于课程设计与期末大作业。项目代码完整、经导师指导评审通过,可直接运行,覆盖数据…

2026/9/24 0:00:19 阅读更多 →
C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

第一次在项目里被反射卡住,是在一个老旧的WinForms模块里:几十个类依赖PropertyChanged通知,运行时反射读属性、发通知,每次启动慢半拍不说,一上.NET Native/AOT裁剪模式几乎全面崩盘。后来我把这段逻辑全部改成C#源生…

2026/9/24 0:00:19 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/24 14:34:13 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/24 9:10:42 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/24 14:33:56 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/24 12:50:34 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/24 14:33:48 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/24 12:49:17 阅读更多 →