160. 相交链表 - 题解与详细分析

160. 相交链表 - 题解与详细分析

题目描述

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

图示两个链表在节点 c1 开始相交:

text

A: a1 → a2 ↘ c1 → c2 → c3 ↗ B: b1 → b2 → b3

注意:

  • 题目数据保证整个链式结构中不存在环
  • 函数返回结果后,链表必须保持其原始结构

解题思路

这道题要求找到两个链表的相交节点。由于链表可能长度不同,但相交后的部分长度相同,我们可以通过以下方法解决:

核心思想

  1. 计算链表长度:分别遍历两个链表,得到它们的长度
  2. 对齐起点:让长链表先移动长度差值的步数,使两个链表的剩余长度相等
  3. 同时遍历:两个指针同时移动,比较节点是否相同

为什么这样有效?

  • 如果两个链表相交,那么从相交点到链表末尾的长度是相同的
  • 通过让长链表先走差值步,两个指针会同时到达相交点(如果存在)

代码实现

c

/** * Definition for singly-linked list. * struct ListNode { * int val; * struct ListNode *next; * }; */ struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) { struct ListNode *cur1 = headA; struct ListNode *cur2 = headB; int a = 0; int b = 0; // 计算链表A的长度 while(cur1) { a++; cur1 = cur1->next; } // 计算链表B的长度 while(cur2) { b++; cur2 = cur2->next; } // 重置指针到链表头部 cur1 = headA; cur2 = headB; int c = 0; // 让长链表先移动长度差值的步数 if(a > b) { c = a - b; while(c--) { cur1 = cur1->next; } } else { c = b - a; while(c--) { cur2 = cur2->next; } } // 同时遍历两个链表,寻找相交节点 while(cur1) { if(cur1 == cur2) { return cur1; } cur1 = cur1->next; cur2 = cur2->next; } return NULL; }

代码详解

第一部分:计算链表长度

c

struct ListNode *cur1 = headA; struct ListNode *cur2 = headB; int a = 0; int b = 0; // 计算链表A的长度 while(cur1) { a++; cur1 = cur1->next; } // 计算链表B的长度 while(cur2) { b++; cur2 = cur2->next; }

  • 使用两个指针分别遍历两个链表
  • 计数器 a 和 b 分别记录链表A和链表B的长度

第二部分:对齐起点

c

cur1 = headA; cur2 = headB; int c = 0; if(a > b) { c = a - b; while(c--) { cur1 = cur1->next; } } else { c = b - a; while(c--) { cur2 = cur2->next; } }

  • 重置指针到链表头部
  • 计算长度差值 c
  • 让长链表的指针先移动 c 步,使两个指针后的剩余长度相等

第三部分:寻找相交节点

c

while(cur1) { if(cur1 == cur2) { return cur1; } cur1 = cur1->next; cur2 = cur2->next; } return NULL;

  • 两个指针同时移动
  • 比较节点地址是否相同(注意:是比较节点本身,不是节点值)
  • 找到相同节点则返回,否则返回 NULL

执行过程可视化

以题目示例为例:

text

链表A: a1 → a2 → c1 → c2 → c3 链表B: b1 → b2 → b3 → c1 → c2 → c3

执行步骤:

  1. 计算长度:
    • 链表A长度:5
    • 链表B长度:6
  2. 对齐起点:
    • 长度差:1
    • 链表B指针先移动1步:b2 → b3 → c1 → c2 → c3
  3. 同时遍历:
    • A: a1 → a2 → c1 → c2 → c3
    • B: b2 → b3 → c1 → c2 → c3
    • 在节点c1处相遇,返回c1

优化方案:双指针法

还有一种更巧妙的方法,不需要计算链表长度:

c

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) { if (headA == NULL || headB == NULL) return NULL; struct ListNode *pA = headA; struct ListNode *pB = headB; while (pA != pB) { pA = pA == NULL ? headB : pA->next; pB = pB == NULL ? headA : pB->next; } return pA; }

原理:

  • 两个指针分别遍历两个链表
  • 当指针到达链表末尾时,切换到另一个链表的头部
  • 如果两个链表相交,指针会在相交点相遇
  • 如果不相交,指针会同时到达NULL

复杂度分析

长度差法

  • 时间复杂度:O(m+n),其中m和n分别是两个链表的长度
  • 空间复杂度:O(1),只使用常数级别的额外空间

双指针法

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

关键点总结

  1. 比较节点地址:注意是比较节点本身(指针地址),不是节点值
  2. 链表无环:题目保证链表无环,简化了问题
  3. 保持原始结构:算法不应该修改链表结构
  4. 边界情况:处理空链表的情况

测试用例考虑

  1. 不相交的链表:返回NULL
  2. 完全相同的链表:返回头节点
  3. 一个链表是另一个的子集:返回较长链表的头部
  4. 在中间节点相交:返回相交节点
  5. 空链表:返回NULL

应用场景

这种链表相交问题在实际开发中有多种应用:

  1. 内存管理:检测两个数据结构是否共享内存区域
  2. 图算法:在树或图中寻找公共节点
  3. 版本控制系统:寻找代码分支的合并点
  4. 数据库查询:优化连接查询的执行计划

总结

寻找相交链表节点是一个经典的链表问题,掌握两种解法都很重要:

  1. 长度差法:思路直观,容易理解和实现
  2. 双指针法:代码简洁,不需要计算长度

关键技巧:

  • 利用相交后部分长度相同的特性
  • 通过对齐起点来简化比较过程
  • 注意指针操作和边界条件处理

这道题考察了对链表结构的理解和双指针技巧的应用,是面试中常见的问题之一。

Read more

【Linux/C++多进程篇(一) 】一个变两个?揭秘 C/C++ 程序中神奇的“分身术”

【Linux/C++多进程篇(一) 】一个变两个?揭秘 C/C++ 程序中神奇的“分身术”

⭐️在这个怀疑的年代,我们依然需要信仰。 个人主页:YYYing. ⭐️Linux/C++进阶系列专栏:【从零开始的linux/c++进阶编程】 ⭐️其他专栏:【linux基础】【数据结构与算法】【从零开始的计算机网络学习】 系列上期内容:【Linux/C++文件篇(一) 】标准I/O与文件I/O基础  系列下期内容:【Linux/C++多进程篇(二) 】万字解析linux系统编程之进程间通信 (IPC) 目录 前言:        多进程理论基础 一、为什么要引入多进程 二、多进程相关概念 三、进程的内存管理 四、进程与程序的区别 五、进程的种类 六、进程PID 七、特殊的进程 八、linux中有关进程的指令 九、进程状态的切换

By Ne0inhk

为什么 Java 一行代码,JVM 要执行 4 条指令?(99% Java 开发没真正看过)

为什么 Java 一行代码,JVM 要执行 4 条指令?(99% Java 开发没真正看过) * JVM 字节码实战:深入解析 System.out.println 的执行原理 * 一、前言:为什么需要了解字节码? * 二、JVM 运行时数据区全景 * 2.1 关键区域说明 * 2.2 栈帧结构详解(重点) * 三、Java 程序的执行链路 * 3.1 完整执行流程 * 3.2 关键认知 * 四、实战:使用 javap 分析 class 文件 * 4.1 环境准备 * 4.

By Ne0inhk
Java外功精要(2)——Spring IoC&DI

Java外功精要(2)——Spring IoC&DI

1.IoC(控制反转) 1.1 Spring Ioc VS Servlet 在上文:Java外功基础(1)——Spring Web MVC中,很形象地模拟出使用Spring"建造房子"的大概流程。使用Spring建造房子不需要像Servlet那样烧制每一块砖,只需要从Spring中取出一个个提前预制好的组件然后组装即可。换言之:Spring是包含了大量工具的IoC容器 1.2 IoC解析 1.2.1 IoC概述 概念:IoC(Inversion of Control,控制反转),是一种设计原则,用于减少代码间的直接依赖关系。传统编程中,调用者通常主动创建和管理被调用者的生命周期,而 IoC 将这种控制权交给外部容器或框架,由容器负责对象的创建、依赖注入和管理 示例一:传统编程模式 classCar{protectedFramework

By Ne0inhk

汉化版IDEA 更换 JDK 版本全教程,超详细!

汉化版IDEA 更换 JDK 版本全教程,超详细! 在 Java 开发中,我们经常需要根据项目需求切换不同的 JDK 版本(比如从 JDK8 升级到 JDK17,或降级到 JDK11)。对于使用汉化版 IntelliJ IDEA的小伙伴,本文全程采用中文菜单 / 选项描述,不改动任何非必要配置,手把手教你完成 JDK 切换,全程避坑! 一、场景 1:设置全局默认 JDK(所有新项目生效) * 若未打开任何项目:直接点击 IDEA 启动界面的「文件 → 新项目设置 → 新项目的结构」(2020 + 版本); * 若已打开项目:点击「文件 → 项目结构」。 1. 在弹出的「项目结构」窗口左侧,

By Ne0inhk