​​​​​1​​​​​​​141. 环形链表1 题目

​​​​​1​​​​​​​141. 环形链表

给你一个链表的头节点 head ,判断链表中是否有环。

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

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

提示:

  • 链表中节点的数目范围是 [0, 104]
  • -105 <= Node.val <= 105
  • pos 为 -1 或者链表中的一个 有效索引 。

进阶:你能用 O(1)(即,常量)内存解决此问题吗?

2 我的想法

判断环的思想就是,用一个追踪指针从头遍历到尾指针,存在环的意思是:如果在pos所指示的位置有追踪指针,那么追踪指针和尾指针会交汇。

也就是说:

1.如果有环,尾指针指向pos所在节点

2.如果没有环,第一种情况是单个元素,第二种情况是尾指针一定不指向pos所在节点

/*** Definition for singly-linked list.* class ListNode {*     int val;*     ListNode next;*     ListNode(int x) {*         val = x;*         next = null;*     }* }*/
public class Solution {public boolean hasCycle(ListNode head) {}
}

难道写一个尾指针.next是否与pos节点一致的判断就好了吗?

具体要怎么实现呢,其实链表对应节点所在val值要是一样的呢?我怎么表示pos节点,我这样的想法可行吗,如何写出合适的java代码?如果是用遍历的话,空间复杂度是O(1),时间复杂度是O(n)对吗?写一个for循环加断?

3 实现

关于你的想法的分析

你提到 "写一个尾指针.next 是否与 pos 节点一致的判断",这个思路存在几个问题:

  1. pos 不可用:题目明确说明 pos 是系统内部使用的,不会作为参数传递给我们的函数,所以我们无法直接获取和使用 pos 值
  2. 尾指针不存在:在有环的链表中,不存在传统意义上的 "尾指针"(因为链表会循环,没有终点)
  3. val 值可能重复:如你所担心的,不同节点可能有相同的 val 值,无法通过 val 来判断是否是同一个节点

所以我们需要一种不依赖 pos、不依赖节点值、且空间效率高的方法。

详细解题思路

最经典的解法是 "快慢指针法",也叫 "龟兔赛跑算法",思路如下:

  1. 指针初始化

    • 定义两个指针,都从链表头部出发
    • 慢指针 (slow) 每次只移动 1 步
    • 快指针 (fast) 每次移动 2 步
  2. 核心逻辑

    • 如果链表中没有环:快指针会先到达链表末尾(指向 null)
    • 如果链表中有环:两个指针最终一定会在环中相遇
      • 因为快指针比慢指针移动得快,进入环后会逐渐追上慢指针
  3. 终止条件

    • 若快指针到达 null,返回 false(无环)
    • 若快慢指针相遇,返回 true(有环)

算法为什么有效?

假设链表存在环,当慢指针刚进入环时,快指针已经在环内某个位置。此时可以看作快指针在追赶慢指针:

  • 快指针每次比慢指针多走 1 步
  • 相当于慢指针静止,快指针以每秒 1 步的速度追赶
  • 只要在环内,快指针最终一定会追上慢指针
/*** Definition for singly-linked list.* class ListNode {*     int val;*     ListNode next;*     ListNode(int x) {*         val = x;*         next = null;*     }* }*/
public class Solution {public boolean hasCycle(ListNode head) {// 处理边界情况:空链表一定没有环if (head == null) {return false;}// 初始化快慢指针ListNode slow = head;      // 慢指针,每次走1步ListNode fast = head.next; // 快指针,每次走2步// 当快慢指针不相等时继续循环while (slow != fast) {// 快指针到达末尾,说明没有环// 注意要先判断fast是否为null,再判断fast.nextif (fast == null || fast.next == null) {return false;}// 慢指针移动1步slow = slow.next;// 快指针移动2步fast = fast.next.next;}// 循环结束,说明快慢指针相遇,存在环return true;}
}

复杂度分析

  • 时间复杂度:O (n),其中 n 是链表中的节点数

    • 无环情况:快指针会遍历 n/2 个节点就到达末尾
    • 有环情况:最坏情况下,两个指针在环中最多遍历 n 个节点就会相遇
  • 空间复杂度:O (1),只使用了两个指针,不随链表长度变化,满足题目进阶要求

其他可能的解法(但不满足 O (1) 空间)

  1. 哈希表法

    • 遍历链表,将每个节点存入哈希表
    • 每次访问节点前先检查哈希表,如果已存在则有环
    • 空间复杂度 O (n),不满足进阶要求
  2. 标记法

    • 遍历链表时给每个访问过的节点做标记
    • 如果遇到已标记的节点则有环
    • 需要修改节点结构,实际中不常用

综上,快慢指针法是解决这个问题的最优解,既高效又节省空间。

4 两种解法

先回顾核心问题:为什么判断链表有环不能靠 “节点值” 或 “pos”?

在理解方法前,先明确两个关键前提(也是你之前担心的点):

  1. pos 是 “系统内部标记”,我们拿不到:题目里的 pos 只是用来给我们看 “环接在哪”,实际写代码时,函数参数只有head,根本没有 pos 这个值,所以没法用 “尾指针.next == pos 节点” 这种思路。
  2. 节点值可能重复,不能作为判断依据:比如链表是1→2→1→2(有环),两个 “1” 节点值相同,但却是不同节点,靠值判断会误判;反之,不同节点值也可能组成环,所以 “值” 完全不可靠。

这也是为什么我们需要「哈希表」和「快慢指针」这两种不依赖 “值” 或 “pos” 的方法 —— 它们判断的是 “节点本身是否被重复访问”。

方法一:哈希表法 ——“给走过的节点拍个照,再遇到就是有环”

核心思路:用 “哈希表” 当 “相册”,记录所有走过的节点

就像你逛公园时,每经过一个景点就拍张照存手机里。如果逛着逛着,发现眼前的景点 “手机里早就有照片了”,说明你绕回了之前的路(有环);如果一直走到公园出口(链表末尾,head == null),都没重复照片,说明没环。

步骤拆解(对应代码)

我们用 Java 的HashSet(哈希表的一种)来实现 “相册”,因为HashSet有个特性:添加重复元素时会返回false,这正好帮我们判断 “是否见过这个节点”。

public class Solution {public boolean hasCycle(ListNode head) {// 1. 初始化哈希表(相册),存的是“节点本身”,不是节点值!Set<ListNode> seen = new HashSet<ListNode>();// 2. 遍历链表:只要当前节点不为null(没走到出口),就继续走while (head != null) {// 3. 尝试把当前节点加入哈希表://    - 如果添加失败(返回false),说明之前见过这个节点→有环,返回true//    - 如果添加成功,说明是第一次见,继续往下走if (!seen.add(head)) {return true;}// 4. 移动到下一个节点(逛下一个景点)head = head.next;}// 5. 走出循环说明head == null(走到出口),没环,返回falsereturn false;}
}
关键细节:为什么存 “ListNode” 而不是 “val”?
  • ListNode(节点对象):每个节点对象在内存中都有唯一的 “地址”,哈希表判断重复时,比较的是 “地址”,能确保 “同一个节点才会被判定为重复”。
  • val(节点值):如之前说的,值可能重复,会导致 “不同节点被误判为重复”,比如1→2→1(无环),第二个 “1” 会被误判为重复,返回错误的true
复杂度理解
  • 时间复杂度 O (N):最坏情况是 “链表无环”,我们要把所有 N 个节点都遍历一遍,每个节点的 “添加” 和 “判断” 操作在哈希表中是 O (1),所以总时间是 O (N)。
  • 空间复杂度 O (N):最坏情况是 “链表无环”,我们要把 N 个节点都存进哈希表,所以空间是 O (N)—— 这也是它的缺点,不如快慢指针省空间。

方法二:快慢指针法(Floyd 判圈算法)——“让兔子和乌龟赛跑,追上就是有环”

核心思路:用两个速度不同的指针,模拟 “兔子(快)” 和 “乌龟(慢)” 在链表上跑
  • 如果链表没环:兔子跑得比乌龟快,会先跑到链表末尾(fast == nullfast.next == null),永远追不上乌龟。
  • 如果链表有环:兔子会先进入环,然后在环里绕圈;等乌龟也进入环后,兔子因为速度快,总会在某个时刻追上乌龟(两个指针指向同一个节点)。
步骤拆解(对应代码)

先明确指针规则:

  • 慢指针(乌龟):每次走 1 步(slow = slow.next
  • 快指针(兔子):每次走 2 步(fast = fast.next.next
public class Solution {public boolean hasCycle(ListNode head) {// 1. 处理边界:空链表(head==null)或只有1个节点(head.next==null),肯定没环if (head == null || head.next == null) {return false;}// 2. 初始化指针:慢指针从head出发,快指针从head.next出发(关键细节,后面解释)ListNode slow = head;ListNode fast = head.next;// 3. 循环:只要快慢指针没相遇,就继续跑while (slow != fast) {// 4. 检查快指针是否到末尾:如果fast或fast.next是null,说明没环if (fast == null || fast.next == null) {return false;}// 5. 乌龟走1步,兔子走2步slow = slow.next;fast = fast.next.next;}// 6. 跳出循环→快慢指针相遇,说明有环,返回truereturn true;}
}
关键细节 1:为什么初始时快指针要从head.next出发,而不是和慢指针一起从head出发?

这是为了适配while循环的 “先判断,后执行” 逻辑:

  • 如果两个指针都从head出发:初始时slow == fastwhile (slow != fast)的条件直接不满足,循环不执行,直接返回false—— 但此时如果链表有环(比如head自己指向自己),就会误判。
  • 快指针从head.next出发:初始时slow = headfast = head.nextslow != fast,循环能正常开始;即使链表是head自环(head.next = head),第一次循环时:
    • slow会走到head.next = head
    • fast会走到head.next.next = head.next = head
    • 此时slow == fast,跳出循环返回true,判断正确。

如果想用 “两个指针都从head出发”,可以把while改成do-while(先执行,后判断),代码如下(逻辑完全等价,只是循环方式不同):

public boolean hasCycle(ListNode head) {if (head == null) return false;ListNode slow = head, fast = head;// do-while:先移动,再判断是否相遇do {// 快指针到末尾,没环if (fast == null || fast.next == null) return false;slow = slow.next;fast = fast.next.next;} while (slow != fast);// 相遇,有环return true;
}
关键细节 2:为什么快指针走 2 步,不是 3 步、4 步?

核心是 “快指针速度比慢指针快”,走 2 步是最简洁的选择:

  • 走 2 步时,每次循环快慢指针的距离会减少 1(比如初始距离 1,下次距离 0;初始距离 2,下次距离 1,再下次距离 0),必然会相遇。
  • 走 3 步、4 步也能实现,但代码会更复杂(比如要多判断fast.next.next是否为 null),且没有性能优势,所以 2 步是最优选择。
复杂度理解
  • 时间复杂度 O (N)
    • 无环情况:快指针走 N/2 步就到末尾(因为每次走 2 步),时间 O (N)。
    • 有环情况:最坏是 “环很小,乌龟进环前要走很多步”,但总体来看,快指针最多绕环 1 圈就能追上乌龟,总步数还是 O (N)。
  • 空间复杂度 O (1):只用到slowfast两个指针,不管链表多长,都只占 2 个指针的空间,完全符合题目 “O (1) 内存” 的进阶要求。

两种方法的对比:怎么选?

对比维度哈希表法快慢指针法
核心逻辑记录已访问节点,看是否重复速度差导致相遇
时间复杂度O(N)O(N)
空间复杂度O (N)(需要存节点)O (1)(只需要两个指针)
代码简洁度较简洁(依赖哈希表 API)稍复杂(需处理指针初始化)
适用场景除了判断环,还想记录节点只需要判断环,追求省空间

实际面试中,快慢指针法是更优的选择—— 因为它满足 O (1) 空间,也是面试官更想考察的思路(能体现对链表特性的深入理解);哈希表法更偏向 “直观思路”,适合作为辅助理解的方法。

通过这两个方法的拆解,你应该能彻底明白 “判断链表有环” 的核心逻辑了~ 下次遇到类似问题,不管是用哈希表还是快慢指针,都能清晰地写出代码啦!

【解释:if (!seen.add(head))】

我们来详细解释一下你看不懂的这行代码:if (!seen.add(head))

这句话的作用

这句话的意思是:尝试把当前节点head添加到哈希表seen中,如果添加失败(说明这个节点已经在哈希表中存在了),就返回true(表示有环)

逐步拆解

  1. seen是一个HashSet<ListNode>类型的集合,用来存储我们已经访问过的节点

  2. seen.add(head)这个方法的特性:

    • 当我们往 HashSet 中添加元素时,如果这个元素之前不存在,就会添加成功,返回true
    • 如果这个元素之前已经存在,就会添加失败,返回false
  3. !是逻辑非运算符,用来取反:

    • 如果seen.add(head)返回true(添加成功,是新节点),那么!true就是false,不会进入 if 语句
    • 如果seen.add(head)返回false(添加失败,节点已存在),那么!false就是true,会进入 if 语句,执行return true

举个生活化的例子

这就像我们去游乐园玩,每个景点门口有个登记簿:

  • 第一次到某个景点,我们在登记簿上写下名字(添加成功),继续游玩
  • 如果走到一个景点,发现登记簿上已经有我们的名字了(添加失败),说明我们绕了一圈又回到了曾经去过的地方,这就证明游乐园的路线是环形的

整个代码的逻辑流程

  1. 从头节点开始遍历链表
  2. 每到一个节点,就尝试把它加入哈希表
  3. 如果添加失败(节点已存在),说明有环,返回true
  4. 如果添加成功,就继续访问下一个节点
  5. 直到遍历完所有节点(head == null),说明没有环,返回false​​​​​​​

这样通过哈希表记录访问过的节点,就能判断链表是否存在环了,是不是很容易理解呢?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/922120.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/922120.shtml
英文地址,请注明出处:http://en.pswp.cn/news/922120.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux 正则表达式详解(基础 + 扩展 + 实操)

Linux 正则表达式详解&#xff08;基础 扩展 实操&#xff09; 正则表达式&#xff08;Regular Expression&#xff0c;简称 RE&#xff09;是 Linux 文本处理的核心工具&#xff0c;用于定义字符匹配模式&#xff0c;配合 grep、sed、awk 等工具可实现文本过滤、查找、替换等…

Json-rpc通信项目(基于C++ Jsoncpp muduo库)

一、介绍RPC RPC&#xff08;Remote Procedure Call&#xff09;远程过程调用&#xff0c;一种通过网络从远程计算器上请求服务&#xff0c;而不需要了解底层网络通信细节&#xff0c;RPC可以使用多种网络协议进行通信&#xff0c;并且在TCP/IP网络四层模型中跨越了传输层和应…

RL【9】:Policy Gradient

系列文章目录 Fundamental Tools RL【1】&#xff1a;Basic Concepts RL【2】&#xff1a;Bellman Equation RL【3】&#xff1a;Bellman Optimality Equation Algorithm RL【4】&#xff1a;Value Iteration and Policy Iteration RL【5】&#xff1a;Monte Carlo Learnin…

Redis是什么?一篇讲透它的定位、特点与应用场景

Redis是什么&#xff1f;一篇讲透它的定位、特点与应用场景 1. Redis的定义与核心概念 1.1 什么是Redis&#xff1f; Redis&#xff08;Remote Dictionary Server&#xff09; 是一个开源的、基于内存的数据结构存储系统&#xff0c;可以用作数据库、缓存和消息代理。Redis由…

一款免费开源轻量的漏洞情报系统 | 漏洞情报包含:组件漏洞 + 软件漏洞 + 系统漏洞

工具介绍 bug_search一款免费开源轻量的漏洞情报系统 基于python3 Amis2.9 开发&#xff0c;仅依赖Flask,requests&#xff0c;无需数据库&#xff0c;Amis是百度开源的低代码前端框架漏洞情报包含&#xff1a;组件漏洞 软件漏洞 系统漏洞 增加邮件发送消息报警功能增加钉钉…

详解在Windows系统中生成ssl证书,实现nginx配置https的方法

目录一、下载安装OpenSSL二、证书生成三、修改nginx配置总结Nginx 是一个高性能的HTTP和反向代理web服务器&#xff0c;在进行web项目开发时&#xff0c;大多都是使用nginx对外提供web服务。HTTPS &#xff08;全称&#xff1a;Hypertext Transfer Protocol Secure [5]&#xf…

AI视觉算法中的OpenCV API (二)

视频写入 (FourCC, VideoWriter)​ 1. VideoWriter_fourcc - 视频编码器四字符代码 # OpenCV 3.x, 4.x fourcc cv2.VideoWriter_fourcc(M,J,P,G)fourcc cv2.VideoWriter_fourcc(*H264)fourcc cv2.VideoWriter_fourcc(*MJPG) ​FourCC​&#xff1a; 代表 ​Four ​Charac…

分享| 2025年版AIGC数字人实验室解决方案教学资源解析

AIGC数字人实验室解决方案构建了涵盖基础层、平台环境层与资源层的多层次教学架构&#xff0c;依托150平方米的实体空间与60人并行授课的规模化支持&#xff0c;为学生提供了技术实践与创新的高效平台。其教学资源体系覆盖AIGC文本生成、图像生成、数字人应用与智能体开发四大核…

内存大(巨)页

一、大&#xff08;巨&#xff09;页 大&#xff08;巨&#xff09;页&#xff0c;很好理解&#xff0c;就是的大的页。说这个大页前&#xff0c;得先把计算机中内存的管理简单说明一下&#xff0c;否则可能对于一些新手或者把操作系统中内存管理的方法的开发者不太友好。最早的…

langgraph astream使用详解

langgraph中graph的astream&#xff08;stream&#xff09;方法分别实现异步&#xff08;同步&#xff09;流式应答&#xff0c;在langgraph-api服务也是核心方法&#xff0c;实现与前端的对接&#xff0c;必须要把这个方法弄明白。该方法中最重要的参数是stream_mode&#xff…

【C++】模板进阶:非类型参数、模板特化与分离编译

目录 1. 非类型模板参数 2. 模板的特化 3. 分离编译 1. 非类型模板参数 模板参数分类类型形参与非类型形参。 类型形参即&#xff1a;出现在模板参数列表中&#xff0c;跟在class或者typename之类的参数类型名称。 非类型形参&#xff0c;就是用一个常量作为类(函数)模板…

栈-1047.删除字符串中的所有相邻重复项-力扣(LeetCode)

一、题目解析 1、反复执行重复项删除操作 2、s仅由小写英文字母组成 二、算法原理 该题并不难&#xff0c;难的是能不能想到用栈这个数据结构解题 解法&#xff1a;栈模拟 横着看起来不好理解&#xff0c;我们把它竖起来&#xff0c;是不是和消消乐很类似&#xff0c;两两消…

【每日算法】移除元素 LeetCode

双指针方法是解决数组或链表问题中非常高效的技巧之一&#xff0c;尤其适用于原地修改数组或减少时间复杂度的场景。以下是对双指针方法的详细讲解。1. 双指针方法的核心思想双指针方法通常使用两个指针&#xff08;或索引&#xff09;在数组或链表中协同工作&#xff0c;通过一…

Android 项目:画图白板APP开发(六)——分页展示

本篇将介绍如何为我们的画板应用添加分页展示功能&#xff0c;让用户可以创建多个画布并在它们之间轻松切换。这章没有啥知识点的讲解&#xff0c;主要介绍一下每页保存的数据结构是什么样的。 一、ListView 多页数据的管理我们使用ListView。之前有文章讲过ListView这里就不多…

智能眼镜产品成熟度分析框架与评估

引言 当前(2025年9月12日),智能眼镜(Smart Glasses)市场正处于快速演进阶段,从早期的新奇设备向主流消费电子转型。AI整合、AR显示和多模态交互的进步推动了这一转变。根据最新数据,2025年AI眼镜发货量预计达686万台,同比增长265%,全球市场规模从2024年的约19.3亿美元…

(网络编程)网络编程套接字 UDP的socket API 代码解析

网络编程基础 为什么需要网络编程?--丰富的网络资源 用户在浏览器中,打开在线视频网站,如优酷看视频,实质是通过网络,获取到网络上的一个视频资源。与本地打开视频文件类似,只是视频文件这个资源的来源是网络。 相比本地资源来说,网络提供了更为丰富的网络资源:所谓的网络资源…

【STM32】状态机(State Machine)

这篇博客介绍 状态机&#xff08;State Machine&#xff09;&#xff0c;适合用于嵌入式开发、驱动开发、协议解析、按键识别等多种场景。 一、什么是状态机&#xff08;State Machine&#xff09;&#xff1f; 状态机&#xff08;State Machine&#xff09;是一种用于描述系统…

深度学习在离岗检测中的应用

离岗检测技术正逐步成为现代企业精细化管理和安全生产的重要工具。这项基于计算机视觉和人工智能的应用&#xff0c;通过自动化、实时化的监测方式&#xff0c;有效提升了工作纪律性和运营效率&#xff0c;为项目管理者和企业提供了创新的监管解决方案。在许多工作场景中&#…

Spring缓存(二):解决缓存雪崩、击穿、穿透问题

1. 缓存穿透问题与解决方案 1.1 什么是缓存穿透 缓存穿透是指查询一个不存在的数据&#xff0c;由于缓存中没有这个数据&#xff0c;每次请求都会直接打到数据库。 如果有恶意用户不断请求不存在的数据&#xff0c;就会给数据库带来巨大压力。 这种情况下&#xff0c;缓存失去了…

PHP 与 WebAssembly 的 “天然隔阂”

WebAssembly&#xff08;简称 WASM&#xff09;是一种低级二进制指令格式&#xff0c;旨在为高级语言提供高性能的编译目标&#xff0c;尤其在浏览器环境中实现接近原生的执行效率。它主要用于前端性能密集型场景&#xff08;如游戏引擎、视频编解码、3D 渲染等&#xff09;&am…