如何判断两个链表是否相交?

判断两个链表是否相交,核心是检测节点内存地址是否相同,而非值相同。常用方法有两种:一是哈希集合法,遍历链表A将节点存入集合,再遍历链表B检查节点是否已存在,时间复杂度O(m+n),空间复杂度O(m);二是双指针法,先计算两链表长度并让长链表指针先走长度差步,再同步遍历直至指针相遇或为空,时间复杂度O(m+n),空间复杂度O(1)。双指针法更优,因无需额外空间。需注意边界情况:空链表不相交;尾节点不同则不相交;尾节点相同则必相交;交点可能在头节点或一链表为另一子链表。两种方法均基于节点身份比较,而非值比较,确保判断准确。

如何判断两个链表是否相交?

判断两个链表是否相交,核心在于它们是否共享同一个节点,而非仅仅节点的值相同。最常见且高效的方法有两种:一种是利用哈希集合(或称散列表)来记录遍历过的节点,另一种则是巧妙地运用双指针,在不使用额外空间的情况下找出交点。理解这两种方法,以及它们各自的优劣,对于解决这类链表问题至关重要。

解决方案

在我看来,解决链表相交问题,双指针法无疑是最为优雅且空间复杂度最优的方案。它不需要任何额外的存储空间,仅仅通过指针的移动就能找到答案。

这个方法的大致思路是这样的:假设我们有两个链表A和B。

计算长度并对齐:我们先分别遍历链表A和链表B,计算出它们的长度

lenA

lenB

。同时,在遍历到末尾时,我们还可以顺便检查一下它们的尾节点是否相同。如果

headA

的尾节点和

headB

的尾节点不同,那它们肯定不相交,直接返回

null

就行了。这个小技巧能帮我们快速排除掉很多不相交的情况,挺实用的。

如果尾节点相同,那它们就可能相交了。接下来,我们要让两个链表从“同一起跑线”开始遍历。找出两个链表的长度差

diff = abs(lenA - lenB)

。然后,让较长的那个链表的头指针先向前移动

diff

步。这样一来,两个链表的当前指针就和它们的交点(如果存在的话)保持了相同的距离。

同步遍历寻找交点:现在,两个指针(一个在链表A上,一个在链表B上)已经“对齐”了。我们让它们以相同的速度,一步一步地向前移动。在每次移动之后,都比较这两个指针指向的节点是否是同一个。一旦

pointerA == pointerB

,那么这个节点就是它们的第一个交点。我们直接返回这个节点。如果两个指针都走到了链表末尾(即都变成了

null

)还没相遇,那就说明它们虽然尾节点相同,但实际上并没有相交(这种情况通常不会发生,因为如果尾节点相同,它们必然会在某个地方相交)。但为了逻辑严谨,我们也可以在循环结束后返回

null

用伪代码来描述一下,大概是这样:

class ListNode:    def __init__(self, x):        self.val = x        self.next = Nonedef getIntersectionNode(headA: ListNode, headB: ListNode) -> ListNode:    if not headA or not headB:        return None    # 1. 计算长度并找到尾节点    currA, lenA = headA, 0    while currA.next:        currA = currA.next        lenA += 1    lenA += 1 # 加上最后一个节点    currB, lenB = headB, 0    while currB.next:        currB = currB.next        lenB += 1    lenB += 1 # 加上最后一个节点    # 如果尾节点不同,则不相交    if currA != currB:        return None    # 2. 对齐链表头    ptrA, ptrB = headA, headB    if lenA > lenB:        for _ in range(lenA - lenB):            ptrA = ptrA.next    elif lenB > lenA:        for _ in range(lenB - lenA):            ptrB = ptrB.next    # 3. 同步遍历寻找交点    while ptrA != ptrB:        ptrA = ptrA.next        ptrB = ptrB.next    return ptrA # 此时ptrA和ptrB指向同一个交点,或者都为None(如果一开始就相同)

这个方法的时间复杂度是 O(m+n),因为我们需要遍历两个链表两次(一次计算长度,一次寻找交点),但空间复杂度是 O(1),这是它最大的优势。

为什么不能仅仅通过节点的值来判断相交?

这个问题我记得刚接触链表的时候,就挺让我着迷的,也经常看到有朋友在这里犯迷糊。简单来说,判断链表是否相交,我们看的是它们是否共享“同一个物理节点”,也就是内存地址完全一致的那个对象,而不是仅仅节点里面存储的

val

值是不是一样。

想想看,如果两个链表各自有一个节点,它们的值都是

5

,但它们是两个完全独立的节点对象,在内存里占据不同的位置,那它们显然不能算作相交。相交的定义是,从某个节点开始,它们后面的所有节点都是一模一样的,就像两条路汇合成了一条路。如果只比较值,你可能会遇到两个完全独立的链表,它们有很多节点的值都碰巧一样,但它们从未真正“合并”过。

举个例子:链表A: 1 -> 2 -> 3链表B: 1 -> 2 -> 4

这里面,A和B都有值是1和2的节点。但它们是各自独立的节点。如果我们只看值,可能会误以为它们在1或2处相交了,但实际上并没有。它们是两条完全分开的路径。

所以,核心是“对象身份”的比较,而非“值”的比较。这也是为什么在代码里我们判断的是

ptrA == ptrB

,而不是

ptrA.val == ptrB.val

。搞清楚这一点,对于理解链表这种数据结构的本质,我觉得挺重要的。

链表相交的常见误区与边界情况

在处理链表相交问题时,除了前面提到的“只比较值”这个误区,还有一些边界情况和思路上的小陷阱,我觉得也值得聊聊。

空链表: 如果其中一个链表是空的(

headA

headB

null

),它们显然不可能相交。这时候直接返回

null

就行了。这是最简单的边界情况,但有时容易在写代码时漏掉。不相交但尾节点不同: 大多数不相交的链表,它们的尾节点也肯定是不同的。在这种情况下,我们前面双指针法里提到的“先检查尾节点”的优化就能派上用场了。如果尾节点不同,直接判断不相交,省去了后续的遍历。这算是一个小小的性能提升点。不相交但尾节点相同? 这个听起来有点矛盾,但实际上是不存在的。如果两个链表真的在某个点相交了,那么从那个交点往后,它们就是同一条链表了,自然它们的尾节点也必须是同一个。所以,如果尾节点不同,它们必然不相交;如果尾节点相同,它们必然相交。这个逻辑关系非常重要。相交在头节点: 有时候两个链表可能在它们的第一个节点就相交了,也就是

headA == headB

。这种情况下,我们的双指针法也能正确处理,因为它会直接在第一步就发现

ptrA == ptrB

并返回这个头节点。一个链表是另一个的子链表: 比如链表A是

1 -> 2 -> 3

,链表B是

2 -> 3

,并且B的头节点

2

就是A的第二个节点

2

。这种也算相交,并且交点就是

2

。我们的双指针法同样能正确识别。

理解这些边界情况,能帮助我们在设计算法时考虑得更周全,避免一些意想不到的bug。说到底,链表操作,细节决定成败。

空间换时间:哈希集合的策略

虽然我个人更偏爱双指针的优雅,但哈希集合(或者说用

Set

结构)来解决链表相交问题,在某些场景下,比如面试时想快速给出一个正确答案,或者对空间复杂度不那么敏感时,它是一个非常直观且易于实现的好方法。

它的核心思想很简单:

遍历并存储一个链表的所有节点:我们选择其中一个链表(比如链表A),从头开始遍历它。每遍历到一个节点,就把这个节点本身(也就是它的内存地址引用)存储到一个哈希集合里。哈希集合的特点就是查找效率非常高,通常是 O(1) 的平均时间复杂度。

遍历另一个链表并查找:接着,我们开始遍历另一个链表(比如链表B)。对于链表B的每一个节点,我们都去哈希集合里查询一下,看看这个节点是否已经存在。如果找到了,那恭喜你,这个节点就是两个链表的第一个交点。我们直接返回它。如果遍历完链表B的所有节点,都没有在哈希集合里找到任何一个节点,那就说明这两个链表不相交,返回

null

用伪代码表示:

def getIntersectionNodeWithHashSet(headA: ListNode, headB: ListNode) -> ListNode:    if not headA or not headB:        return None    visited_nodes = set() # 创建一个哈希集合    # 遍历链表A,将所有节点加入哈希集合    currA = headA    while currA:        visited_nodes.add(currA)        currA = currA.next    # 遍历链表B,查找是否存在于哈希集合中    currB = headB    while currB:        if currB in visited_nodes: # 如果找到,说明相交            return currB        currB = currB.next    return None # 遍历完B也没找到,说明不相交

优缺点分析:

优点:实现简单直观: 逻辑非常清晰,不容易出错。时间复杂度优秀: 同样是 O(m+n),因为需要遍历两个链表一次,哈希集合的插入和查找操作平均是 O(1)。缺点:空间复杂度较高: 需要额外的 O(m) 或 O(n) 空间来存储一个链表的所有节点。对于内存敏感的场景,这可能是一个问题。

所以,在选择解决方案时,如果对空间有严格要求,双指针法是首选;如果更看重代码的简洁性和开发效率,或者空间不是瓶颈,哈希集合法也是一个非常不错的选择。这两种方法各有千秋,理解它们背后的原理和适用场景,才能在实际问题中做出最合适的选择。

以上就是如何判断两个链表是否相交?的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1370017.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年12月14日 10:09:57
下一篇 2025年12月14日 10:10:15

相关推荐

  • 如何删除列表中的重复元素并保持顺序?

    利用集合记录已见元素,遍历列表时仅添加首次出现的项,从而实现去重并保持原有顺序。 删除列表中的重复元素并保持原有顺序,核心思路是利用一个辅助的数据结构(比如集合Set)来记录我们已经见过的元素。当遍历原始列表时,如果当前元素尚未在集合中出现,我们就将其添加到新的结果列表中,并同时更新集合;如果已经出…

    2025年12月14日
    000
  • 谈谈 Python 的 GIL(全局解释器锁)及其对多线程的影响

    GIL是CPython中限制多线程并行执行的互斥锁,确保同一时刻只有一个线程运行字节码,导致计算密集型任务无法充分利用多核CPU;但在I/O密集型任务中,因线程会释放GIL,多线程仍可提升吞吐量;为应对GIL限制,开发者应根据任务类型选择合适的并发策略:I/O密集型使用threading或async…

    2025年12月14日
    000
  • 如何管理Python项目的依赖?

    答案:Python依赖管理核心在于隔离与精确控制,通过虚拟环境避免依赖冲突,结合pip、requirements.txt或更先进的Poetry、Rye等工具实现环境可复现;虚拟环境确保项目独立,现代工具如Poetry利用pyproject.toml和锁定文件提升依赖解析与一致性,处理复杂冲突时需版本…

    2025年12月14日
    000
  • 如何判断一个数是否是质数?

    判断一个数是否是质数,核心是检查其是否有除1和自身外的因子,只需试除到平方根即可,因若存在大于平方根的因子,则必有对应的小于等于平方根的因子,故只需用2和3到√n的奇数试除,可高效判断。 判断一个数是否是质数,核心在于检查它除了1和自身之外,是否还有其他正整数因子。最直观的方法就是尝试用2到这个数平…

    2025年12月14日
    000
  • 如何理解Python的描述符(Descriptor)?

    描述符通过实现__get__、__set__等方法控制属性访问,解决属性验证、计算等重复逻辑问题;数据描述符因实现__set__而优先级高于实例字典,非数据描述符则可被实例属性覆盖,这一机制支撑了property、方法绑定等核心功能;自定义如TypeValidator类可复用验证逻辑,利用__set…

    2025年12月14日
    000
  • 如何进行Python项目的性能剖析(Profiling)?

    性能剖析是通过工具定位Python代码中耗时和资源消耗大的部分。首先用cProfile进行函数级分析,找出“时间大户”,再用line_profiler深入分析热点函数的逐行执行情况。两者结合实现从宏观到微观的优化。此外,还需关注内存(memory_profiler)、I/O(手动计时、数据库分析)和…

    2025年12月14日
    000
  • 如何部署一个机器学习模型到生产环境?

    部署机器学习模型需先序列化存储模型,再通过API服务暴露预测接口,接着容器化应用并部署至云平台或服务器,同时建立监控、日志和CI/CD体系,确保模型可扩展、可观测且可持续更新。 部署机器学习模型到生产环境,简单来说,就是让你的模型真正开始“干活”,为实际用户提供预测或决策支持。这并非只是把模型文件复…

    2025年12月14日
    000
  • Python 中的浅拷贝与深拷贝:区别与应用场景

    浅拷贝创建新容器但共享内部元素,深拷贝递归复制所有层级确保完全独立。Python中通过切片、copy()实现浅拷贝,copy.deepcopy()实现深拷贝,前者高效但修改嵌套可变元素会影响原对象,后者开销大但隔离彻底。 Python中的浅拷贝与深拷贝,核心在于它们处理复合对象内部元素的方式不同。简…

    2025年12月14日
    000
  • 谈谈你对Python上下文管理器的理解(with语句)。

    Python的with语句通过上下文管理器协议(__enter__和__exit__方法)实现资源的自动管理,确保其在使用后无论是否发生异常都能被正确释放。它简化了try…finally结构,广泛应用于文件操作、数据库事务、线程锁、临时状态更改和测试mock等场景,提升代码可读性与可靠性…

    2025年12月14日
    000
  • 如何使用Python进行机器学习(Scikit-learn基础)?

    答案:Scikit-learn提供系统化机器学习流程,涵盖数据预处理、模型选择与评估。具体包括使用StandardScaler等工具进行特征缩放,SimpleImputer处理缺失值,OneHotEncoder编码类别特征,SelectKBest实现特征选择;根据问题类型选择分类、回归或聚类模型,结…

    2025年12月14日
    000
  • Python的自省(Introspection)能力是什么?

    Python自省能力的核心机制包括type()、dir()、getattr()、hasattr()、setattr()、isinstance()等内置函数及inspect模块,它们使程序能动态检查对象类型、属性、方法和调用栈。通过这些工具,代码可在运行时探索结构、实现动态调度、构建插件系统与ORM框…

    2025年12月14日
    000
  • 你在Python项目开发中遵循哪些编码规范(PEP 8)?

    PEP 8是Python编码规范的核心,提升代码可读性与团队协作效率。我遵循4空格缩进、合理命名、适当行长、清晰空白符等原则,并结合black、flake8等工具自动化格式化。在团队中推行统一风格,避免风格争议,提升维护效率。同时灵活应对特殊情况,如使用# noqa处理例外,尊重遗留代码风格。除PE…

    2025年12月14日
    000
  • 什么是猴子补丁(Monkey Patch)?有什么风险?

    猴子补丁是一种运行时动态修改类或模块行为的技术,允许在不改动源码的情况下替换、添加或删除函数、方法和属性,常见于Python、Ruby等动态语言。其核心优势在于即时性和无侵入性,适用于热修复、测试模拟、扩展第三方库及反向移植等场景。通过示例可见,MyClass的original_method在运行时…

    2025年12月14日
    000
  • Python中的垃圾回收机制是如何工作的?

    Python的垃圾回收机制由引用计数和分代垃圾回收共同构成,前者实时释放无引用对象,后者周期性清理循环引用,两者协同确保内存高效管理。 Python的垃圾回收机制,简而言之,就是一套自动管理内存的系统,它负责识别那些程序不再使用的对象,并将其占据的内存空间释放,以便后续可以重新分配。这套机制主要通过…

    2025年12月14日
    000
  • 如何对字典列表进行排序?

    使用sorted()函数配合key参数和lambda表达式可轻松对字典列表排序,支持单键、多键、升降序及缺失值处理,且Python排序稳定,能保持相同键值元素的相对顺序。 说起来,给一堆字典排个序,这事儿在Python里其实挺顺手的。核心思路就是用那个 sorted() 函数,然后关键在于给它一个 …

    2025年12月14日
    000
  • 如何处理Python中的异常?自定义异常如何实现?

    Python通过try-except-finally实现异常处理,可捕获特定错误并执行相应逻辑,else在无异常时运行,finally始终执行用于资源清理;通过继承Exception类可创建自定义异常,提升业务错误的清晰度与处理精度。 Python处理异常的核心机制是 try-except 语句块,…

    2025年12月14日
    000
  • 如何使用Python进行内存管理和优化?

    Python内存管理基于引用计数和分代垃圾回收,可通过gc模块干预回收行为,但优化核心在于使用高效数据结构、生成器、__slots__及内存分析工具定位瓶颈。 Python的内存管理主要依赖引用计数和分代垃圾回收,但真正的优化往往需要深入理解数据结构、对象生命周期以及利用专业的分析工具。核心在于识别…

    2025年12月14日
    000
  • Python中的多进程与多线程如何选择?

    CPU密集型任务应选多进程,因GIL限制多线程无法并行计算;I/O密集型任务宜用多线程,因等待期间可释放GIL实现高效并发。 在Python中决定使用多进程还是多线程,关键在于你的任务类型:是CPU密集型还是I/O密集型。如果你的程序大部分时间都在进行计算,那多进程几乎是唯一能真正利用多核CPU的途…

    2025年12月14日
    000
  • 如何实现二叉树的遍历?

    答案是二叉树遍历分为前序、中序、后序和层序四种,分别采用递归或迭代实现,用于系统访问节点,处理空节点需加判断,广泛应用于表达式求值、序列化、LCA查找等场景。 二叉树的遍历,说白了,就是按照某种特定的规则,把树上的每一个节点都“走”一遍,访问一遍。最核心的无非是三种深度优先遍历(前序、中序、后序)和…

    2025年12月14日
    000
  • 如何实现一个LRU缓存?

    LRU缓存通过哈希表与双向链表结合,实现O(1)读写与淘汰;哈希表快速定位节点,双向链表维护访问顺序,最近访问节点移至头部,超出容量时移除尾部最久未使用节点。 实现LRU缓存的核心思路,在于巧妙地结合哈希表(Hash Map)和双向链表(Doubly Linked List),以达到O(1)时间复杂…

    2025年12月14日
    000

发表回复

登录后才能评论
关注微信