当前位置: 首页 > news >正文

数据结构算法学习:LeetCode热题100-链表篇(上)(相交链表、反转链表、回文链表、环形链表、环形链表 II)

文章目录

  • 简介
  • 160. 相交链表
  • 206. 反转链表
  • 234. 回文链表
  • 141. 环形链表
  • 142. 环形链表 II
  • 个人学习总结

简介

在 LeetCode 热题 100 中,链表相关的问题占据了重要席位。本篇博客作为“链表篇(上)”,将精选其中五道最具代表性的题目:相交链表、反转链表、回文链表、环形链表以及环形链表 II。我们将逐一剖析它们的解题思路,从直观的哈希表解法,到精妙绝伦的双指针技巧,深入探讨每种方法背后的思想与权衡。希望通过本次学习,能掌握这几道题的解法,更能提炼出解决链表问题的通用范式与核心思想,为后续更复杂的数据结构学习打下坚实的基础。

160. 相交链表

题目描述:
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
图示两个链表在节点 c1 开始相交:
在这里插入图片描述
示例:
在这里插入图片描述

标签提示: 哈希表、链表、双指针

解题思想
这个方法的核心思想是利用哈希集合的特性,将“判断两个链表是否在某节点相交”的问题,巧妙地转化为“在一个集合中查找元素是否存在”的问题。

可以把它想象成一个“登记与核对”的过程:

  • 登记:首先,我们遍历第一个链表(链表 A),将其中的每一个节点(对象引用)都存入一个哈希集合中。哈希集合的特点是,它能够以极高的效率判断一个元素是否已经存在于集合中。
  • 核对:然后,我们开始遍历第二个链表(链表 B)。对于 B 中的每一个节点,我们都去哈希集合中查询它是否已经被“登记”过。
  • 找到交集:如果在核对过程中,我们发现链表 B 的某个节点已经存在于哈希集合中,那么这个节点就是两个链表的第一个公共节点,也就是它们的交点。
  • 无交集:如果我们将链表 B 的所有节点都核对完毕,都没有在集合中找到匹配项,那么说明这两个链表从始至终都没有任何公共节点,即不相交。

这种思路通过“空间换时间”的方式,将一个需要复杂指针操作的问题,简化为了两次简单的线性遍历和哈希查找。

解题步骤

  1. 初始化哈希集合:创建一个空的哈希集合,用于存储第一个链表的所有节点。
  2. 遍历链表 A 并存入集合:从头到尾遍历链表 A,将访问到的每一个节点都存入哈希集合中。
  3. 遍历链表 B 并进行核对:从头到尾遍历链表 B。对于链表 B 中的每一个节点,都去哈希集合中查询它是否存在。
    • 如果存在,则该节点就是两个链表的交点,直接返回该节点。
    • 如果不存在,则继续检查链表 B 的下一个节点。
  4. 处理无交集情况:如果链表 B 遍历完毕,仍未在集合中找到任何节点,则说明两个链表不相交,返回 null。

实现代码

public class Solution {public ListNode getIntersectionNode(ListNode headA, ListNode headB) {Set<ListNode> set = new HashSet<ListNode>();ListNode tmp = headA;while(tmp != null){set.add(tmp);tmp = tmp.next;}tmp = headB;while(tmp != null){if(set.contains(tmp)){return tmp;}tmp = tmp.next;}return null;}
}

复杂度分析

  • 时间复杂度: O(m + n)
    • m 是链表 A 的长度,n 是链表 B 的长度。
    • 算法包含两个独立的遍历过程:首先遍历链表 A(耗时 O(m)),然后遍历链表 B(耗时 O(n))。在遍历链表 B 的过程中,哈希集合的 contains 操作平均时间复杂度为 O(1)。因此,总的时间复杂度为 O(m + n)。
  • 空间复杂度: O(m)
    • 算法的主要空间开销来自于哈希集合。在最坏的情况下,我们需要将链表 A 的所有 m 个节点都存入这个集合中。因此,空间复杂度为 O(m)。

206. 反转链表

题目描述:
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例:
在这里插入图片描述
标签提示: 递归、链表

解题思想
这个方法的核心是原地迭代反转。我们不需要创建任何新的节点,而是在遍历链表的同时,直接修改每个节点的 next 指针,让它指向前一个节点,从而实现整个链表的反转。(也就是头插法)
为了在修改指针后还能继续遍历,我们需要三个关键角色:一个指向前一个节点的指针 pre,一个指向当前节点的指针 curr,以及一个临时变量来保存当前节点的下一个节点 next,防止“断链”。

解题步骤

  1. 初始化指针:定义两个指针 pre 和 curr。pre 初始化为 null,用于记录反转后的链表头;curr 初始化为 head,用于遍历原链表。
  2. 迭代反转链表:开启一个 while 循环,条件为 curr 不为 null。在循环中,对每个 curr 节点执行以下操作:
    • 保存后继节点:创建临时变量 next 存储 curr.next,防止断链。
    • 反转指针:将 curr.next 指向 pre。
    • 移动指针:将 pre 更新为 curr,curr 更新为 next,为下一次迭代做准备。
  3. 返回新头节点:当循环结束时,curr 指向 null,而 pre 指向了反转后链表的第一个节点。直接返回 pre 即可。

实现代码

class Solution {public ListNode reverseList(ListNode head) {// 利用尾插法ListNode pre = null;ListNode curr = head;while(curr != null){ListNode next = curr.next;curr.next = pre;pre = curr;curr = next;}return pre;}
}

复杂度分析

  • 时间复杂度: O(n)
    • 其中 n 是链表的长度。算法只需要对链表进行一次完整的遍历,循环内的所有操作都是常数时间的,因此总时间复杂度为 O(n)。
  • 空间复杂度: O(1)
    • 算法只使用了固定数量的额外指针变量(pre, curr, next),这些变量的内存占用与链表的长度无关。因此,空间复杂度为常数级别 O(1),实现了原地反转。

234. 回文链表

题目描述:
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。

示例:
在这里插入图片描述
标签提示: 栈、递归、链表、双指针

解题思想
这个方法的核心是利用快慢指针找到链表的中点,并在寻找中点的过程中,同步地将链表的前半部分进行反转。这样,当快指针到达末尾时,慢指针正好位于中点,而链表的前半部分已经被反转。接下来,我们只需同时遍历反转后的前半部分和链表的后半部分,依次比较它们的值即可判断是否为回文。
这个方法巧妙地将“找中点”和“反转前半部分”两个步骤合二为一,并且全程只使用了常数个额外指针,实现了 O(1) 的空间复杂度。

解题步骤

  1. 初始化指针:定义三个指针。fast 和 slow 都初始化为 head,用于寻找中点;pre 初始化为 null,用于在遍历时反转前半部分链表。
  2. 寻找中点并反转前半部分:开启一个 while 循环,条件为 fast 不为 null 且 fast.next 不为 null。在循环中:
    • 快指针 fast 前进两步。
    • 慢指针 slow 前进一步,并在前进前,将自己与 pre 指针所代表的反转部分进行连接,从而完成对当前节点的反转。
  3. 处理奇数长度链表:循环结束后,如果 fast 不为 null,说明链表长度为奇数,此时 slow 指向中间节点。中间节点无需比较,将 slow 向后移动一位,跳过它。
  4. 比较前后两半部分:开启第二个 while 循环,同时遍历 slow(后半部分)和 pre(反转后的前半部分)。逐个比较节点的值,如果不相等,则不是回文,返回 false。
  5. 返回结果:如果比较循环正常结束,说明所有对应节点的值都相等,链表是回文,返回 true。

实现代码

class Solution {public boolean isPalindrome(ListNode head) {// 使用快慢指针ListNode pre = null;ListNode fast = head;ListNode slow = head;while(fast != null && fast.next != null){fast = fast.next.next;// 翻转前半部分ListNode next = slow.next;slow.next = pre;pre = slow;slow = next;}if(fast != null){slow = slow.next;}while(slow != null){if(slow.val != pre.val){return false;}pre = pre.next;slow = slow.next;}return true;}
}

复杂度分析

  • 时间复杂度: O(n)
    • 其中 n 是链表的长度。第一个循环(快慢指针)遍历了链表的一半,第二个循环(比较)也遍历了链表的一半。总的时间复杂度是 O(n/2) + O(n/2) = O(n)。
  • 空间复杂度: O(1)
    • 算法只使用了固定数量的额外指针变量(pre, fast, slow, next),没有使用与链表长度相关的额外存储空间,因此空间复杂度为常数级别 O(1)。

141. 环形链表

问题描述:
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。

示例:
在这里插入图片描述

标签提示: 哈希表、链表、双指针

解题思想
这个方法的核心是经典的“快慢指针”算法,也被称为“龟兔赛跑”算法。

想象一下,在一个有环的跑道上,一个跑得快的人和一个跑得慢的人同时从同一点出发。只要跑道是环形的,跑得快的人最终一定会从后面追上并超过跑得慢的人,即两者会再次相遇。如果跑道是直的(无环),那么跑得快的人会先到达终点,两者永远不会相遇。

在链表中,我们用两个指针 fast 和 slow 来模拟这个过程。fast 指针每次走两步,slow 指针每次走一步。如果链表中存在环,fast 和 slow 指针必然会在环内相遇;如果链表没有环,fast 指针会先到达链表末尾。

解题步骤

  1. 边界情况处理:如果链表为空或只有一个节点,它不可能有环,直接返回 false。
  2. 初始化指针:创建两个指针,慢指针 slow 指向头节点 head,快指针 fast 指向头节点的下一个节点 head.next。这样初始化可以确保 slow 和 fast 在循环开始前不相等。
  3. 循环遍历:开启一个 while 循环,循环条件是 slow 和 fast 不相遇。在循环中:
    • 检查无环情况:如果 fast 指针或 fast 的下一个节点为 null,说明快指针已经到达链表末尾,链表无环,返回 false。
    • 移动指针:慢指针 slow 向前移动一步,快指针 fast 向前移动两步。
  4. 返回结果:如果 while 循环正常结束,说明 slow 和 fast 指针相遇了,因此链表中必然存在环,返回 true。

实现代码

public class Solution {public boolean hasCycle(ListNode head) {// 快慢指针,有环,快指针一定与慢指针相遇if(head == null || head.next == null){return false;}ListNode slow = head;ListNode fast = head.next;while(slow != fast){if(fast == null || fast.next == null){return false;}slow = slow.next;fast = fast.next.next;}return true;}
}

复杂度分析

  • 时间复杂度: O(n)

    其中 n 是链表的节点数。在无环的情况下,快指针会先到达终点,最多遍历 n/2 个节点。在有环的情况下,快慢指针在进入环后,它们之间的距离每次缩小 1,因此会在 O(n) 步内相遇。所以总时间复杂度为 O(n)。

  • 空间复杂度: O(1)

    算法只使用了两个额外的指针变量 slow 和 fast,没有使用与链表长度相关的额外存储空间,因此空间复杂度为常数级别 O(1)。

142. 环形链表 II

问题描述:
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。

示例:
在这里插入图片描述

标签提示: 哈希表、链表、双指针

解题思想
本次主要分享双指针法,其中哈希表法,就是通过创建一个HashSet用于记录遍历的节点,当遍历到的节点已在HashSet中,表示有环,遍历到了尾部表示没有环。

快慢指针法的核心思想是将问题分解为两个阶段:
阶段一:判断链表是否存在环
阶段二:寻找环的入口节点
阶段一:判断链表是否存在环(快慢指针相遇法)
想象一下,一个有环的链表就像一个环形跑道。我们设置两个指针:

  • 慢指针:每次只移动一步。
  • 快指针:每次移动两步。
  • 如果链表没有环:快指针会先到达链表末尾,其 next 会为 null,循环终止,我们确定无环。
  • 如果链表有环:快指针因为速度更快,一定会从后面“追上”慢指针,最终在环内的某个位置相遇。这就像在环形跑道上,跑得快的人总会套圈跑得慢的人。因此,只要 slow == fast,我们就能断定链表存在环。

阶段二:寻找环的入口节点(数学推导)
这是整个算法最精妙的部分。当快慢指针在环内相遇后,我们如何定位环的入口呢?
也就是fast指针步长变为1,和slow一样,那么再次相遇就会在入口相遇!
这里需要一点简单的数学推导。如下图所示:
在这里插入图片描述

解题步骤

  1. 处理边界情况:如果链表为空或只有一个节点,必然无环,直接返回 null。
  2. 初始化指针:创建 slow 和 fast 两个指针,初始都指向头节点 head。
  3. 寻找相遇点:进入 while 循环。在循环中,slow 前进一步,fast 前进两步。如果 slow 和 fast 相遇,说明存在环,跳出循环。
  4. 判断无环情况:循环结束后,检查 fast 或 fast.next 是否为 null。如果是,说明快指针已到达链表末尾,链表无环,返回 null。
  5. 定位环入口:
    • 将 fast 指针重新指向头节点 head。slow 指针保持在相遇点不动。
    • 进入第二个 while 循环,slow 和 fast 指针都每次前进一步。
    • 当 slow 和 fast 再次相遇时,相遇点即为环的入口。
  6. 返回结果:返回 slow(或 fast,此时它们指向同一节点)。

实现代码

public class Solution {public ListNode detectCycle(ListNode head) {if(head == null || head.next == null){return null;}ListNode fast = head;ListNode slow = head;while(fast != null && fast.next != null){slow = slow.next;fast = fast.next.next;if(slow == fast){break;}}if(fast == null || fast.next == null){return null;}fast = head;while(slow != fast){slow = slow.next;fast = fast.next;}return slow;}
}

复杂度分析

  • 时间复杂度:O(N)

    阶段一(寻找环):在最坏情况下(无环),快指针需要遍历整个链表,时间复杂度为 O(N)。在有环的情况下,慢指针在进入环前最多走 L 步,进入环后,快慢指针的距离会不断缩小,最多再走 C 步就会相遇。因此,阶段一的时间复杂度为 O(L + C),而 L + C 小于等于链表总长度 N,所以是 O(N)。
    阶段二(寻找入口):一个指针从头走 L 步,另一个从相遇点走 L 步,它们会在入口相遇。这个过程最多遍历 L 个节点,时间复杂度为 O(L),即 O(N)。
    综合:总的时间复杂度为 O(N) + O(N) = O(N)。

  • 空间复杂度:O(1)

    整个算法只使用了常数个额外指针变量(slow, fast),没有使用与链表长度相关的额外存储空间。因此,空间复杂度为 O(1),表现非常出色。

个人学习总结

通过对这五道经典链表题目的学习,我提炼出以下几点核心感悟:

  1. 双指针是核心:本次学习的最大收获是双指针思想的普适性。无论是解决相交链表、回文链表还是环形链表,它都提供了 O(1) 空间复杂度的最优解,是链表问题的“万能钥匙”。
  2. 空间与时间的权衡:通过对比哈希表和双指针,我深刻理解了算法设计中“空间换时间”的权衡。追求 O(1) 的空间优化往往是面试中的加分项。
  3. 基础操作是基石:“反转链表”不仅是独立的知识点,更是解决“回文链表”的关键步骤。这提醒我,扎实的基础是构建复杂解法的根基。
  4. 化繁为简的策略:面对“环形链表 II”这类难题,“分而治之”的思想至关重要。将其分解为“找环”和“找入口”两个步骤,问题便迎刃而解。
http://www.dtcms.com/a/486352.html

相关文章:

  • STC亮相欧洲区块链大会,碳资产RWA全球化战略迈出关键一步
  • 使用Electron创建helloworld程序
  • 建设校园网站国外研究现状2020网络公司排名
  • DataEase v2 连接 MongoDB 数据源操作说明-MongoDB BI Connector用户创建
  • PHP 8.0+ 编译器级优化与语言运行时演进
  • 网站运营培训网站被百度收录吗
  • 升级到webpack5
  • 【MySQL】MySQL `JSON` 数据类型介绍
  • 通过hutool生成xml
  • vue.config.js 文件功能介绍,使用说明,对应完整示例演示
  • 无极分期网站临沂做网络优化的公司
  • Vue3的路由Router【7】
  • DOM 实例
  • 网站安全建设需求分析报告重庆有哪些科技骗子公司
  • Springboot AOP Aspect 拦截中 获取HttpServletResponse response
  • 【深度学习理论基础】什么是蒙特卡洛算法?有什么作用?
  • 网站建设商虎小程序就业网站建设
  • 从留言板开始做网站企业网站建设代理加盟
  • USB——UVC简介
  • cocosCreator导出Web-Mobile工程资源加载时间分析
  • SpringCloud系列(53)--SpringCloud Sleuth之zipkin的搭建与使用
  • 虚拟主机做视频网站可以吗网络规划的主要步骤
  • 【sqlite】xxx.db-journal是什么?
  • Ubuntu 搭建 Samba 文件共享服务器完全指南
  • ubuntu server版本安装vmtool
  • 《Redis库基础使用》
  • 网站转应用济南网站优化推广公司电话
  • 探索libsignal:为Signal提供强大加密保障的开源库
  • PIL与OpenCV双线性插值实现差异导致模型精度不够踩坑
  • 逆合成孔径雷达成像的MATLAB算法实现