[{"content":"","date":"2026-04-03T18:24:30Z","permalink":"https://iamxurulin.github.io/p/spring-boot--vue-%E5%89%8D%E5%90%8E%E7%AB%AF%E8%81%94%E8%B0%83%E8%B8%A9%E5%9D%91%E8%AE%B0%E5%BD%95/","title":"Spring Boot + Vue 前后端联调踩坑记录"},{"content":"","date":"2026-04-02T18:43:59Z","permalink":"https://iamxurulin.github.io/p/%E5%93%88%E5%B8%8C%E5%AF%B9%E7%A7%B0_%E9%9D%9E%E5%AF%B9%E7%A7%B0%E5%8A%A0%E5%AF%86%E6%95%B0%E5%AD%97%E7%AD%BE%E5%90%8D%E4%B8%80%E6%AC%A1%E6%80%A7%E8%AE%B2%E6%B8%85/","title":"哈希、对称_非对称加密、数字签名，一次性讲清"},{"content":"","date":"2026-04-01T19:17:35Z","permalink":"https://iamxurulin.github.io/p/%E5%88%AB%E5%86%8D%E6%90%9E%E6%B7%B7%E4%BA%86redisjedislettucespring-data-redisredissonspring-session-redis-%E4%B8%80%E6%AC%A1%E8%AE%B2%E9%80%8F/","title":"别再搞混了！Redis、Jedis、Lettuce、Spring Data Redis、Redisson、Spring Session Redis 一次讲透"},{"content":"","date":"2026-04-01T14:12:15Z","permalink":"https://iamxurulin.github.io/p/java%E7%BA%BF%E7%A8%8B%E4%B8%8E%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/","title":"Java线程与操作系统线程的生命周期"},{"content":"项目文件夹叫codeedge-backend，Java包名是codeedgebackend，Maven的artifactId是codeedge-backend，Spring配置里还有个spring.application.name叫codeedge-backend。\n这几个到底有啥区别？必须要对上吗？\n其实，这几个是完全不同维度、不同作用、没有任何强制绑定关系的配置，给谁用、管什么、能不能乱改，差别还是挺大的。\n我就用 codeedge-backend 项目当例子，先从最核心、最容易踩语法坑的 Java 包名说起。\nJava包名 com.codeedgexu.codeedgebackend 这是Java语言强制规范的代码组织方式，是给Java编译器、JVM、Spring Boot看的，用来唯一标识代码类，和项目功能强绑定。\n作用 唯一标识代码类，避免重名冲突 你的controller、config、service这些类，全在com.codeedgexu.codeedgebackend这个包下面。\nJava编译器靠【包名+类名】唯一确定一个类，比如你的启动类全限定名是com.codeedgexu.codeedgebackend.CodeEdgeBackendApplication，这个名字全局唯一，不会和别人的项目重名冲突。\n决定Spring Boot的Bean扫描范围 Spring Boot启动的时候，默认只会扫描启动类所在的包以及它的子包。\n比如你的启动类在com.codeedgexu.codeedgebackend下面，那它只会扫这个包下面的controller、service，把它们注册成Bean。\n如果改了代码的包名，但启动类没跟着搬到新包，Spring 就扫不到新包里的 controller、service 这些类，导致这些类不会被注册成 Bean，项目启动失败。\n小贴士 Java包名里，绝对不能用横杠-。 因为横杠在Java里是减号运算符，不能出现在包名、类名、变量名里，编译器会直接报错。\n这也是为什么我文件夹名可以叫codeedge-backend，但包名必须写成codeedgebackend的根本原因——不是我不想写一样的，是Java语法不允许。\nJava包名的所有部分必须全小写。 这是Java的行业通用规范，虽然语法允许大写，但不专业，且很多代码规范检查工具会报错。\nMaven artifactId codeedge-backend 这是Maven项目的唯一标识，在同一个groupId下必须唯一，用来在Maven仓库里找到你的项目、打包发布、管理依赖。\n作用 唯一标识你的Maven项目，和别人的项目区分开； 项目打包、发布到Maven仓库，全靠这个artifactId； 以后别人要引用你的项目，就是靠这个artifactId写依赖： 1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.codeedgexu\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;codeedge-backend\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.0.1-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 小贴士 上线后如果修改了 artifactId，项目打包出来的jar包名字会变，别人引用你的项目也要同步改artifactId，不然就找不到依赖。\n推荐用kebab-case短横线分隔的格式（比如codeedge-backend），可读性好，Maven全兼容，是行业里90%的项目都会用的写法。\napplication.yml文件中的codeedge-backend 1 2 3 spring: application: name: codeedge-backend 这是Spring Boot应用的官方服务名，核心作用是给微服务注册中心（Nacos）看的，是你这个服务在微服务集群里的唯一标识。\n作用 Nacos服务注册的唯一标识 你的服务启动时，会去Nacos登记，登记的名字就是这个spring.application.name；\n微服务之间调用的标识 以后有其他服务要调用你的接口，就是靠这个名字在Nacos里找到你的服务地址；\n小贴士 同样地，上线后如果修改了这个服务名，Nacos里注册的服务名就变了，其他服务再用旧名字找你，就完全找不到了，微服务调用直接报错。\n推荐和Maven的artifactId保持一致（比如也叫codeedge-backend），这样你在看Nacos控制台的时候，一眼就能对应上“这个服务对应Maven里的哪个项目”，不会混乱。\n","date":"2026-03-27T21:33:09Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%80%E7%AF%87%E6%90%9E%E6%87%82java%E5%8C%85%E5%90%8Dmaven-artifactidspring-application.name%E5%88%B0%E5%BA%95%E6%9C%89%E5%95%A5%E5%8C%BA%E5%88%AB/","title":"一篇搞懂：Java包名、Maven artifactId、Spring application.name到底有啥区别？"},{"content":"结构化输出、TokenStream、SSE、Flux、Reactor这几个概念刚接触时感觉很混乱，本文就来理一理，我用【AI生成一个电商商品详情页】的真实项目场景，把这些概念串起来，从业务需求到底层技术，一次性搞明白。\nLangChain4j结构化输出 结构化输出是LangChain4j里的一个核心功能，能强制大模型返回标准的JSON/POJO组件树对象，而不是一段诸如“我给你加个按钮、加个图片”的自然语言。\n之前你让AI生成商品详情页，它可能返回：\n“好的，我帮你生成了一个详情页，顶部有个轮播图放3张商品图，中间有商品标题、价格、库存，底部有个‘立即购买’的红色按钮\u0026hellip;”\n然后你还得写自己大量文本解析逻辑，甚至还要用正则去提取轮播图数量、按钮颜色，又繁琐又容易出错，而且根本没法直接渲染成 Web 组件。\n用了结构化输出，AI 会直接返回标准 JSON，之后 LangChain4j 会自动把 JSON 转成 Java 对象。 底层默认使用 Jackson（Java 最主流的 JSON 库），只需要提前定义好页面结构的模板类，系统就能自动完成字段映射，完全不用写任何解析代码，直接就能喂给前端的渲染引擎。\nTokenStream + SSE TokenStream是LangChain4j提供的流式API，大模型生成一个Token（大概是一个字、半个JSON键或值），就立刻推给你，不用等整个组件树JSON生成完；\nSSE（Server-Sent Events），把TokenStream里的JSON片段，实时推给前端的渲染引擎，实现“轮播图先出来、标题再出来、按钮最后出来”的逐个渲染效果。\n之前你让AI生成一个复杂的Web应用，要等10-30秒整个JSON生成完，才能一次性渲染给用户，用户需要在空白的预览区页面长时间等待。\n用了TokenStream + SSE，AI生成完一个完整的组件JSON片段，前端就立刻渲染这个组件，用户能实时看到进度。\nFlux \u0026amp; Reactor Reactor是Spring官方的响应式编程框架，专门用来处理高并发、异步、流式的场景；\nFlux是Reactor的核心类之一，代表0到N个元素的异步序列，刚好对应AI生成的JSON片段流（一个JSON片段就是一个元素）。\n如果用传统的同步编程，几百个用户同时请求生成Web应用，你需要开几百个线程，每个线程都要等10-30秒AI生成完，服务器的CPU、内存直接就爆了，根本扛不住。\n用了 Flux + Reactor，就能彻底解决这个问题：\n它是完全异步非阻塞 + 事件驱动的模型 —— 发起 AI 调用后，线程立刻释放去处理其他请求，绝不死等；等 AI 生成 Token 后，再主动通知线程来处理推送，1 个线程能同时管理几百个请求，所以只用 10-20 个线程就能扛住几百甚至上千的并发。\n另外，它还自带背压机制：前端网速慢，服务端就自动放慢推送速度；前端快，就加快推送，永远不会让前端浏览器因为数据堆积而卡死。\n一张图搞懂 ","date":"2026-03-26T20:42:49Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%80%E7%AF%87%E6%90%9E%E6%87%82langchain4j%E7%BB%93%E6%9E%84%E5%8C%96%E8%BE%93%E5%87%BAtokenstreamssefluxreactor%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88/","title":"一篇搞懂：LangChain4j结构化输出、TokenStream、SSE、Flux、Reactor到底是什么？"},{"content":"面试官问了这么一道场景题：\n“平台用了Caffeine本地缓存、Redis分布式缓存、MySQL数据库，现在有十万个用户同时访问，相当于每个用户对应一个请求，这多级缓存该怎么设计？”\n多级缓存就是“分层扛压”，把十万用户的请求，从快到慢、从近到远，一层层分流，别让所有请求都直接打MySQL。\n设计方案 定好每一层的职责 缓存层级 职责 放什么数据 容量/速度 Caffeine本地缓存 扛超级热点中的超级热点，大幅减少 Redis 压力 访问频率 Top10 的极致热门数据（比如首页轮播爆款、正在进行的秒杀活动） 容量小（几百MB）、速度极快（纳秒级） Redis分布式缓存 扛全局通用数据，挡MySQL 全平台的业务数据（比如商品详情、用户信息、订单数据） 容量大（几十GB）、速度快（毫秒级） MySQL数据库 数据最终兜底，保证数据不丢 所有的原始业务数据 容量无限、速度慢（秒级） 缓存预热 平台刚启动的时候，Caffeine和Redis里都是空的，如果这时候十万用户同时进来，所有请求都会直接打MySQL，直接把数据库打垮。\n为了解决这个问题，考虑采用缓存预热：\n平台启动前，先做两级缓存预热：\n把访问频率最高的Top1000 个全量热门 Key（比如全平台热门商品、活动规则），提前从 MySQL 里查出来，全量加载到 Redis； 把其中访问频率最高的Top10 个超级热点 Key（比如首页轮播商品、正在进行的爆款活动），额外加载到每台服务器的 Caffeine 本地缓存，作为最外层的扛压层。 缓存雪崩、击穿、穿透 缓存雪崩：十万个Key同时过期 比如你给所有Key都设置了“1小时过期”，结果十万个Key在同一时间全部过期，所有请求直接打MySQL。\n解决这个问题的话，就是别给所有Key都设一样的过期时间，可以设置“1小时 + 0~30分钟的随机值”，让Key的过期时间分散开，避免同时过期；\n对于首页Top10商品这种超级热点数据，还可以设置成“永不过期”，只在数据更新的时候主动删除缓存。\n缓存击穿：超级热门Key突然过期 比如“双11爆款商品详情”这个Key，每秒有十万个请求，结果它突然过期了，十万个请求同时打Redis，发现没有，又同时打MySQL，直接把两者都打垮。\n这个问题也可以对超级热点Key不设过期时间，只在更新时主动删；\n或者加Redis 分布式互斥锁：\n如果 Key 真的过期了，只让第一个抢到锁的请求去查 MySQL，查出来后更新缓存并释放锁；\n其他没抢到锁的请求，每隔 100ms 重试一次查缓存，直到缓存更新完成，彻底避免十万请求同时打 MySQL。\n缓存穿透：查根本不存在的Key 比如有人恶意攻击，每秒发十万个请求查“id=999999999”的商品（这个商品根本不存在），缓存里没有，直接打MySQL，把数据库打垮。\n这个问题优先采用布隆过滤器拦截：\n平台启动时，提前把 MySQL 里所有商品的 id 全量加载到布隆过滤器里完成预热； 用户请求进来，先过布隆过滤器，如果布隆过滤器判定这个商品 id 不存在，直接返回【商品不存在】，完全不会打到缓存和 MySQL； 只有布隆过滤器判定 id 存在，才会继续走【Caffeine→Redis→MySQL】的查询流程。 兜底方案：\n缓存空值。如果查 MySQL 发现这个 Key 真的不存在，就把【空值】缓存到 Redis 里（设置 5 分钟短过期时间），下次再查这个 Key，直接从 Redis 里拿空值返回，避免重复打 MySQL。\n小贴士：\n空值缓存必须设置短过期时间（3-5 分钟），同时限制单 IP 的请求频率，避免恶意攻击生成大量空值缓存撑爆 Redis 内存。\n十万并发下的 Redis 压力分摊方案 十万个请求同时进来，如果都打到单实例 Redis 上，大概率会直接把 Redis 打宕机，必须做以下 3 件事分摊压力：\n热点隔离，本地优先扛压：把访问频率 Top10 的极致超级热点 Key（比如首页轮播爆款、正在进行的秒杀活动），全量放在每台服务器的 Caffeine 本地缓存里。十万请求里 90% 都能直接在本地命中，Redis 的压力直接降 90%。 用 Redis Cluster 集群，把数据分片存储在多个节点上，每个节点只扛一部分请求，避免单节点压力过载； Redis 主节点只负责数据更新的写请求，多个从节点负责处理十万级的读请求，把读压力分摊到多个从节点上。 多实例下的本地缓存一致性 服务基本都是多实例集群部署的（比如 10 台服务器），每台服务器都有自己独立的 Caffeine 本地缓存，很容易出现数据不一致的问题：\n比如在 A 实例更新了商品价格，删了 A 实例的 Caffeine 和 Redis 缓存，但 B 实例的 Caffeine 里还存着旧价格，用户访问 B 实例就会看到脏数据。\n考虑用延迟双删 + Redis 发布订阅的组合方案解决这个问题：\n延迟双删：保证 Redis 和 MySQL 的数据一致 ① 先删除 Redis 缓存，让缓存失效；\n② 再更新 MySQL 数据库，写入新数据；\n③ 等待 1 秒（延迟时间必须略大于你的业务接口平均耗时，根据实际压测结果调整，不能盲目设置），再次删除 Redis 缓存。\n延迟的目的是等其他实例已经从 MySQL 读到旧数据、更新到缓存后，再删一次缓存，避免脏数据回写。\nRedis 发布订阅：保证多实例 Caffeine 的数据一致 ①A实例更新完数据、完成延迟双删后，通过 Redis 的发布订阅功能，给集群里所有服务实例广播一条消息：商品 id=123 的缓存已失效，请删除本地缓存；\n②所有实例收到消息后，立刻删除自己本地Caffeine里对应的Key；\n③下次用户访问任何实例，都会去 Redis 里查询新数据，再更新到本地 Caffeine，保证全集群所有实例的数据一致。\n完整的请求+数据更新全流程 ","date":"2026-03-26T14:59:06Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A3%E5%8D%81%E4%B8%87%E7%94%A8%E6%88%B7%E5%B9%B6%E5%8F%91%E4%B8%8Bcaffeine-redis-mysql%E5%A4%9A%E7%BA%A7%E7%BC%93%E5%AD%98%E6%80%8E%E4%B9%88%E8%AE%BE%E8%AE%A1/","title":"【面试真题拆解】十万用户并发下，Caffeine+Redis+MySQL多级缓存怎么设计？"},{"content":"用户注册登录模块几乎是每一个项目都有的，有一次面试的时候，面试官问：\n你既然说到了登录，那你知道Token是什么吗？\n一句话：\nToken = 用户登录成功后，服务器给你颁发的一张加密的临时电子通行证，最常用的就是 JWT 格式的 Token。\n它本质上就是一段加密字符串，作用就是证明三件事：你是谁、你已登录、你有权限访问这个接口。\n为什么登录必须用 Token？ 因为 HTTP 请求是无状态的——服务器记不住你上一秒是不是登录过。\n每一次请求，对服务器来说都是“陌生人”。\n所以必须给你发一张 Token，让你每次访问都带着，证明你已登录。\n不然你每次点一下页面都要重新输账号密码，根本没法用。\nToken 的工作流程 Token 的作用 Token 和 Session 的区别 存的地方不一样\nSession 把你的身份信息全存在服务器，用户一多就特别占内存；\nToken 是把“电子通行证”直接存在你自己的前端设备里，服务器不用存任何用户状态，只需要验一下通行证的真假就行，几乎没额外负担。 分布式场景的适配能力不一样\nSession 不适合多服务器的分布式场景——就像你在1号窗口办的会员，2号窗口根本不认，得来回同步数据，麻烦得很；\nToken 就没这个烦恼，你手里的通行证全平台通用，不管哪个服务器，只要能验过真伪就直接放行，天生就适配分布式。 说白了，Token 就是为了解决 HTTP 无状态的痛点，给前端和后端之间搭了一座信任的桥梁，不用每次都输账号密码，不用服务器存储大量用户状态，还能完美适配现在的分布式项目。\n","date":"2026-03-26T12:06:32Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A3%E4%BD%A0%E7%9F%A5%E9%81%93%E7%99%BB%E5%BD%95%E7%94%A8%E5%88%B0%E7%9A%84token%E6%98%AF%E4%BB%80%E4%B9%88%E5%90%97/","title":"【面试真题拆解】你知道登录用到的Token是什么吗？"},{"content":"用一个电商场景中常见的用户下单流程，把前后端的所有“器”串起来：\n前后端对比 层级 前端“器” 作用 后端“器” 作用 第一道关卡 路由守卫 页面访问权限控制 过滤器 跨域、编码、协议级过滤 第二道关卡 请求/响应拦截器 统一加Token、统一报错 拦截器 登录校验、权限验证 业务层 - - Controller 执行核心业务逻辑 业务增强层 - - AOP切面 日志、监控、事务增强，不侵入主业务代码 执行层 - - 执行器 异步任务、线程池调度 其实啊，所有的“器”，本质上都是“关注点分离”的体现，即主业务代码只关心“下单”本身，而登录、跨域、日志、异步这些“脏活累活”，交给各种“器”去做。\n","date":"2026-03-25T20:58:53Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%80%E7%AF%87%E6%90%9E%E6%87%82%E5%89%8D%E5%90%8E%E7%AB%AF%E6%89%80%E6%9C%89%E7%9A%84%E5%99%A8/","title":"一篇搞懂前后端所有的“器”"},{"content":"面试官问：\n“你项目里用了SSE做流式输出，为什么选SSE？它的原理你知道吗？为什么不用WebSocket？”\n为什么用SSE做流式输出？ 一句话：\nSSE 是为服务器单向给客户端推数据的场景量身定做的，完美匹配 AIGC 流式打字机的需求，开发简单、好用、运维成本低。\n类比：\nSSE 就像广播电台：电台单向给你播新闻、播音乐，你只能听，不能对着电台说话； WebSocket 就像打电话：你和对方可以双向说话，你一句我一句，实时互动。 SSE流式输出的原理 SSE是Server-Sent Events（服务器发送事件），本质上是一条由服务器控制生命周期的持久化 HTTP 长连接，在数据推送周期内保持打开，不会像普通 HTTP 请求一样响应完成就立刻关闭。\n完整的流程就是：\n客户端发起请求 浏览器向服务器发起一个普通的HTTP GET请求，但请求头里要加 Accept: text/event-stream，告诉服务器：“我要接收流式数据，别给我返回普通网页”；\n服务器建立长连接 服务器收到请求后，不立刻返回完整响应，而是保持这条HTTP连接一直打开，不关闭；\n服务器持续推数据 服务器有新数据了（比如AI生成了一个字），就通过这条打开的连接，把数据推给客户端；\nSSE 有固定的标准文本格式，每段数据以data:开头、\\n\\n结尾，刚好适配大模型逐 Token 生成的流式输出，后端生成一个 Token 就推一次，前端无需额外做格式解析，直接渲染成打字机效果；\n客户端解析数据 浏览器收到数据后，自动解析，触发 onmessage 事件，前端把数据展示出来；\n连接结束或自动重连 如果数据推完了，服务器就会主动关闭连接；\n如果连接意外断开（非手动关闭、非服务器返回 204 状态码），SSE 自带自动重连机制，浏览器会自动重新发起请求，还会通过请求头Last-Event-ID带上最后接收的消息 ID，后端可据此补发丢失的内容，避免 AI 回答断句缺失，建立新连接后继续接收数据。\n为什么不用WebSocket？ WebSocket 是为双向实时交互场景设计的，对于 AI 流式输出这个纯服务器单向推数据的场景，有点那什么杀鸡用牛刀了，并且额外增加了开发、运维成本，完全没有必要。\n对比项 SSE（Server-Sent Events） WebSocket 通信模式 单工，纯单向通信：仅支持服务器向客户端主动推送数据，客户端只能被动接收 全双工，双向通信：客户端和服务器可以同时、双向主动发送数据，无请求 - 响应的限制 协议本质 标准 HTTP 应用层协议，完全兼容所有 HTTP 基础设施（Nginx、CDN、网关、防火墙），无需额外配置 独立的应用层协议，底层基于 TCP，仅握手阶段复用 HTTP，握手完成后需协议升级，部分内网网关、WAF 会拦截升级请求 开发复杂度 极低：浏览器原生提供EventSource API，前端几行代码即可实现；后端仅需开发普通 HTTP 接口保持连接推送数据，无需额外处理协议逻辑 较高：需要自行处理连接握手、心跳保活、断线重连、TCP 粘包拆包、帧解析等逻辑，前后端都需要额外的开发工作量 自动重连 原生支持：浏览器内置自动断线重连机制，无需额外开发 无原生支持：需要自行实现心跳检测和断线重连逻辑 适用场景 服务器单向推送场景：AIGC 流式打字机效果、股票行情推送、直播弹幕、新闻实时更新 双向实时交互场景：即时通讯、实时多人游戏、视频会议、物联网设备双向控制 ","date":"2026-03-25T17:41:32Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A3%E4%B8%BA%E4%BB%80%E4%B9%88%E7%94%A8sse%E5%81%9A%E6%B5%81%E5%BC%8F%E8%BE%93%E5%87%BA%E5%92%8Cwebsocket%E6%9C%89%E5%95%A5%E5%8C%BA%E5%88%AB/","title":"【面试真题拆解】为什么用SSE做流式输出？和WebSocket有啥区别？"},{"content":"面试官问：\n“平时会收发短信吧？你知道短信里那种 xxxxx 的短链接，点击之后是怎么跳转到长链接的？它是怎么生成的？”\n点击短链接后发生了什么？ 两个字：重定向。\n用户点击短链接 比如用户点击 t.cn/xu0324；\n浏览器请求短链接服务器 浏览器向 t.cn 的服务器发送HTTP请求，要访问 /xu0324 这个路径；\n服务器查映射关系 服务器去数据库或者Redis里查，发现 xu0324 对应的原始长链接是 https://www.CodeEdge.com/xxx；\n服务器返回重定向响应 服务器不返回网页内容，而是返回一个 HTTP 302状态码，并在响应头 Location 里填上原始长链接；\n浏览器自动跳转 浏览器收到302状态码，看到 Location 里的长链接，自动发起新的请求，访问原始长链接；\n之后，用户就看到了原始长链接的页面。\n那个短链接是怎么生成的？ 目前主流的生成算法有3种：\n自增ID + Base62编码 用分布式ID生成器（比如雪花算法），生成一个全局唯一的自增数字ID（比如 1, 2, 3, \u0026hellip;），再把这个10进制的数字ID，转换成62进制的字符串（Base62）。\n这种方案因为是自增ID，每个ID对应唯一的Base62字符串，所以绝对唯一，没有哈希冲突，而且不涉及复杂的哈希计算，生成速度极快。\n小贴士 什么是Base62呢？平时用的是10进制（0-9），Base62其实是0-9（10个）、a-z（26个）、A-Z（26个），加起来总共62个字符，所以叫Base62。\n哈希算法+截取 对原始长链接做MD5或SHA-1哈希，得到一个32位（或更长）的哈希字符串，再截取哈希字符串的前6位或8位，作为短链接后缀。\n这种方案对于两个不同的长链接，哈希后的前6位可能会一样，从而导致映射错误；如果发现了冲突，需要换一种截取方式（比如截取中间6位），或者加盐重新哈希，但这样也增加了复杂度。\n随机字符串生成 直接随机生成一个6位的Base62字符串，再去数据库里查，如果这个字符串已经被占用了，就重新生成，直到生成一个没有被占用的。\n这种方案会随着链接越来越多，冲突概率越来越大，可能要重试很多次才能生成一个可用的；并且高并发下，多个请求可能会同时生成同一个随机字符串，导致数据库唯一键冲突，所以不适合高并发场景。\n为什么要用短链接？ 1. 短信的字数限制 因为一条短信最多只能发70个汉字（或160个英文字符），如果直接发一个几百个字符的长链接，一条短信根本发不下，要拆成好几条，成本也会相应增加；如果用短链接，只需要十几个字符，就能完美解决字数限制的问题。\n2. 防拦截、防屏蔽 长链接里经常包含很多参数，很容易被运营商、手机安全软件识别为“营销链接”或“钓鱼链接”，直接拦截，但如果用短链接的话，看起来很干净，被拦截的概率就会大大降低。\n3. 数据统计和追踪 短链接服务器可以记录谁在什么时候、通过什么设备、点击了这个链接，企业通过这些数据，可以分析营销效果、用户画像、转化率等等。\n","date":"2026-03-24T21:34:02Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A3%E5%B9%B3%E6%97%B6%E4%BC%9A%E6%94%B6%E5%8F%91%E7%9F%AD%E4%BF%A1%E5%90%A7%E4%BD%A0%E7%9F%A5%E9%81%93%E7%9F%AD%E4%BF%A1%E9%87%8C%E9%82%A3%E7%A7%8D-codeedge-%E7%9A%84%E7%9F%AD%E9%93%BE%E6%8E%A5%E7%82%B9%E5%87%BB%E4%B9%8B%E5%90%8E%E6%98%AF%E6%80%8E%E4%B9%88%E8%B7%B3%E8%BD%AC%E5%88%B0%E9%95%BF%E9%93%BE%E6%8E%A5%E7%9A%84%E5%AE%83%E6%98%AF%E6%80%8E%E4%B9%88%E7%94%9F%E6%88%90%E7%9A%84/","title":"【面试真题拆解】平时会收发短信吧？你知道短信里那种 `CodeEdge` 的短链接，点击之后是怎么跳转到长链接的？它是怎么生成的？"},{"content":"需求分析 在数据爆炸的时代，业务人员最痛苦的事就是：不会写 SQL，却每天都要查数据。\n我用 Spring Boot 3.2 + 通义千问 API打造了一个AI 数据分析助手，实现了“一句话搞定一切”：\n用户输入一句自然语言 → 系统自动解析意图 → 智能生成 SQL → 执行查询 → 自动出图表 + 专业分析结论\n项目效果演示 1. 自动生成的 SQL\n2. 查询结果数据表\n3. ECharts 自动可视化 + 智能结论\n技术栈 后端：Spring Boot 3.2 + MyBatis-Plus + JdbcTemplate + FastJSON2 + Lombok AI：通义千问 Qwen（RestTemplate 调用） 前端：Vue3（CDN）+ Element Plus + ECharts 5（单文件 index.html） 数据库：MySQL 8（附 schema.sql + 200 条演示数据） 其他：SSE 流式响应、动态数据源、SQL 安全正则校验 核心功能 智能 SQL 生成：结合表结构元数据 + Few-Shot Prompt，准确率大幅提升 SQL 安全校验：自动拦截 DROP/DELETE 等危险操作 流式 SSE 实时推送：意图解析 → SQL 生成 → 查询执行 → 结论生成，每一步都有进度条 ECharts 自动适配：柱状图、折线图、饼图、表格智能选择 智能分析结论：通义千问生成 2-3 句专业总结 支持动态数据源：可随时接入企业已有业务数据库 3 步快速上手 下载项目（文末附源码） 配置环境变量： 1 QWEN_API_KEY=你的通义千问密钥 启动： 1 2 # 先执行 sql/schema.sql 初始化数据库 mvn spring-boot:run 浏览器打开：http://localhost:8080\n可以作为学习 LLM + 数据库落地的案例。\n完整源码 + schema.sql + 使用文档 已打包好：https://download.csdn.net/download/qq_44678890/92757775\n","date":"2026-03-24T16:19:59Z","permalink":"https://iamxurulin.github.io/p/spring-boot--%E9%80%9A%E4%B9%89%E5%8D%83%E9%97%AE-ai-%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90%E5%8A%A9%E6%89%8B%E4%B8%80%E5%8F%A5%E8%AF%9D%E7%94%9F%E6%88%90-sql--%E5%9B%BE%E8%A1%A8--%E6%99%BA%E8%83%BD%E7%BB%93%E8%AE%BA/","title":"Spring Boot + 通义千问 AI 数据分析助手：一句话生成 SQL + 图表 + 智能结论"},{"content":"那下一个问题，面试官接着问：\n“ Java里面的线程池都有哪些参数？”\n我当时就记得“核心线程数、最大线程数”，其他的应该还有5个，支支吾吾答不上来，直接就凉了。\n线程池是用来管理线程的，避免频繁创建和销毁线程带来的性能开销。\n类比一下工厂招工：\n不用线程池 来一个任务就招一个新工人（创建线程），任务干完就辞退（销毁线程），频繁招工辞退，效率极低，还费钱；\n用线程池 工厂提前招好一批固定工人（核心线程），任务来了直接让固定工人干，固定工人忙不过来就把任务放待办队列（工作队列），队列也满了就临时招几个临时工（非核心线程），临时工没活干了过段时间就辞退，效率高，还省钱。\n线程池的7个核心参数 Java线程池的核心类是 ThreadPoolExecutor，它的构造方法有7个参数：\n1. corePoolSize：核心线程数 工厂的正式工数量。\n正式工是工厂的“长期工”，默认情况下，就算没活干，也不会被辞退；\n任务来了，先让正式工干，正式工没满的话，直接创建正式工执行任务。\n2. maximumPoolSize：最大线程数 工厂的正式工+临时工的总数量。\n正式工忙不过来，待办队列也满了，就会招临时工；\n正式工+临时工的总数，不能超过最大线程数。\n3. keepAliveTime：空闲线程存活时间 工厂的临时工没活干的时候，多久会被辞退。\n只有临时工（非核心线程）会被回收，正式工（核心线程）默认不会被回收；\n但是，如果设置了 allowCoreThreadTimeOut(true)，正式工没活干了，过了这个时间也会被辞退。\n4. unit：时间单位 空闲线程存活时间的单位。\n比如 TimeUnit.SECONDS（秒）、TimeUnit.MINUTES（分钟）、TimeUnit.HOURS（小时）。\n5. workQueue：工作队列 工厂的待办任务队列。\n正式工（corePoolSize）都在忙，新任务来了就先放进待办队列，等正式工有空了再从队列里拿任务干。\n6. threadFactory：线程工厂 给工厂的工人起名字的。\n用来创建新线程，给线程设置名字、优先级、是否是守护线程等。\n7. handler：拒绝策略 工厂的任务太多了，正式工、临时工、待办队列都满了，怎么处理新任务，就是靠这个参数定义的。\n线程池的执行流程 提交新任务 工厂来了一个新任务；\n判断核心线程数 先看正式工（corePoolSize）有没有满，没满的话，直接招一个正式工（创建核心线程）执行任务；\n判断工作队列 如果正式工满了，就看待办队列（workQueue）有没有满，没满的话，把任务放进队列，等正式工有空了再拿；\n判断最大线程数 如果队列也满了，就看正式工+临时工的总数（maximumPoolSize）有没有满，没满的话，招一个临时工（创建非核心线程）执行任务；\n执行拒绝策略 如果最大线程数也满了，就执行拒绝策略（handler），处理这个新任务；\n任务执行完 线程执行完任务，会从队列里拿下一个任务继续干；\n空闲线程回收 如果是临时工（非核心线程），没活干的时间超过了 keepAliveTime，就会被回收（辞退）。\n4种默认拒绝策略 拒绝策略 作用 适用场景 AbortPolicy（默认） 直接抛出RejectedExecutionException异常，拒绝执行新任务 对数据一致性要求高的场景，任务不能丢，必须让调用者知道任务被拒绝了 CallerRunsPolicy 由提交任务的线程（调用者线程）自己执行这个任务 对性能要求不高，任务不能丢的场景，比如日志记录 DiscardPolicy 直接丢弃新任务，不抛异常，也不做任何处理 对数据一致性要求不高，丢几个任务没关系的场景，比如非核心的监控数据上报 DiscardOldestPolicy 丢弃队列里最老的任务（等待时间最长的任务），然后把新任务放进队列 对数据一致性要求不高，但想尽量执行新任务的场景 ThreadPoolExecutor 的构造方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import java.util.concurrent.*; /** * 线程池示例 */ public class ThreadPoolDemo { public static void main(String[] args) { // 定义线程池的7个核心参数 ThreadPoolExecutor threadPool = new ThreadPoolExecutor( 5, // corePoolSize：核心线程数（5个正式工） 10, // maximumPoolSize：最大线程数（5个正式工+5个临时工） 60, // keepAliveTime：空闲线程存活时间（60秒） TimeUnit.SECONDS, // unit：时间单位（秒） new ArrayBlockingQueue\u0026lt;\u0026gt;(100), // workQueue：有界队列（容量100） Executors.defaultThreadFactory(), // threadFactory：默认线程工厂 new ThreadPoolExecutor.AbortPolicy() // handler：默认拒绝策略（抛异常） ); // 提交任务 for (int i = 0; i \u0026lt; 200; i++) { final int taskId = i; threadPool.execute(() -\u0026gt; { System.out.println(Thread.currentThread().getName() + \u0026#34; 执行任务：\u0026#34; + taskId); }); } // 关闭线程池 threadPool.shutdown(); } } ","date":"2026-03-22T15:09:10Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A3java%E9%87%8C%E9%9D%A2%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0%E9%83%BD%E6%9C%89%E5%93%AA%E4%BA%9B%E5%8F%82%E6%95%B0/","title":"【面试真题拆解】Java里面的线程池都有哪些参数？"},{"content":"面试官上来就问：\n“ 那个Java里面的Static关键字，一般是怎么用？”\n一句话：\nStatic是“类级别的”，不是“实例级别的”。\n被Static修饰的东西，属于整个类，所有实例共用一份，不用new对象就能直接用；\n而没被Static修饰的东西，属于单个实例，每个实例有自己的一份，必须new对象才能用。\n对比项 静态（Static） 实例（非Static） 归属 属于整个类 属于单个实例 内存位置 方法区（JDK8 之后叫元空间） 堆内存 访问方式 类名.（变量/方法），或（实例.变量/方法）（不推荐） 必须通过实例.变量/方法访问 共享性 所有实例共用一份 每个实例有自己的一份 初始化时机 类加载时初始化 创建实例时初始化 访问限制 只能直接访问静态成员 可以访问静态成员和实例成员 1. 静态变量 静态变量就像是整个类共享的“公共储物柜”，所有实例共用这一个柜子，你放进去的东西，别人也能拿到；而实例变量是每个实例自己的“私人储物柜”，互不干扰。\n举个例子，用静态变量做全局计数器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 /** * 网站访问计数器 */ public class WebsiteCounter { // 静态变量：所有实例共用，统计总访问次数 public static int totalVisitCount = 0; // 实例变量：每个实例自己的，统计单个用户的访问次数 public int userVisitCount = 0; /** * 访问方法 */ public void visit() { // 修改静态变量：总访问次数+1 totalVisitCount++; // 修改实例变量：单个用户访问次数+1 userVisitCount++; } public static void main(String[] args) { // 用户1访问 WebsiteCounter user1 = new WebsiteCounter(); user1.visit(); user1.visit(); System.out.println(\u0026#34;用户1访问次数：\u0026#34; + user1.userVisitCount); // 输出2 System.out.println(\u0026#34;总访问次数：\u0026#34; + WebsiteCounter.totalVisitCount); // 输出2 // 用户2访问 WebsiteCounter user2 = new WebsiteCounter(); user2.visit(); System.out.println(\u0026#34;用户2访问次数：\u0026#34; + user2.userVisitCount); // 输出1 System.out.println(\u0026#34;总访问次数：\u0026#34; + WebsiteCounter.totalVisitCount); // 输出3 } } 2. 静态方法 静态方法是属于类的“公共工具”，不用new对象，直接通过【类名.方法名】就能调用，而实例方法是属于单个实例的，必须new对象之后才能调用。\n举个例子，用静态方法做工具类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 /** * 字符串工具类 */ public class StringUtils { // 私有构造方法：从根源上禁止外部代码 new StringUtils() // 工具类全是静态方法，完全不需要实例化，这样做能强制大家用[类名.方法名]的正确用法 private StringUtils() {} /** * 静态方法：判断字符串是否为空 */ public static boolean isEmpty(String str) { return str == null || str.trim().isEmpty(); } /** * 静态方法：字符串反转 */ public static String reverse(String str) { if (isEmpty(str)) { return str; } return new StringBuilder(str).reverse().toString(); } public static void main(String[] args) { // 直接通过类名调用静态方法，不用new对象 System.out.println(StringUtils.isEmpty(\u0026#34;\u0026#34;)); // 输出true System.out.println(StringUtils.reverse(\u0026#34;hello\u0026#34;)); // 输出olleh } } 3. 静态代码块 静态代码块是类加载的时候执行一次，且只执行一次的代码块，用来初始化静态变量或者做一些类级别的准备工作（比如加载配置文件、初始化数据库连接池）。\n举个例子，用静态代码块初始化静态配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 /** * 配置类 */ public class Config { // 静态变量：数据库连接地址 public static String dbUrl; public static String dbUsername; public static String dbPassword; // 静态代码块：类加载时执行一次，初始化静态变量 static { System.out.println(\u0026#34;静态代码块执行，开始初始化配置...\u0026#34;); // 模拟从配置文件读取配置 dbUrl = \u0026#34;jdbc:mysql://localhost:3306/test\u0026#34;; dbUsername = \u0026#34;root\u0026#34;; dbPassword = \u0026#34;123456\u0026#34;; } // 实例代码块：创建实例时执行，每次new都执行 { System.out.println(\u0026#34;实例代码块执行...\u0026#34;); } // 构造方法：创建实例时执行，每次new都执行 public Config() { System.out.println(\u0026#34;构造方法执行...\u0026#34;); } public static void main(String[] args) { // 第一次访问Config类，触发类加载，静态代码块执行 System.out.println(\u0026#34;数据库地址：\u0026#34; + Config.dbUrl); System.out.println(\u0026#34;---\u0026#34;); // new第一个实例，实例代码块和构造方法执行 new Config(); System.out.println(\u0026#34;---\u0026#34;); // new第二个实例，实例代码块和构造方法执行，但静态代码块不再执行 new Config(); } } 小贴士 静态代码块只在类第一次被加载时执行一次，之后不管new多少个实例，都不会再执行。\n4. 静态内部类 静态内部类是属于外部类的“独立房间”，不用依赖外部类的实例就能直接创建，而非静态内部类是属于外部类实例的“附属房间”，必须先有外部类实例才能创建。\n静态内部类的作用是：\n利用 Java 的类加载时机，实现了懒加载（用的时候才创建）+ 线程安全（JVM 保证）+ 不用加锁（性能高）的完美单例。\n举个例子，用静态内部类实现线程安全的单例模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 /** * 单例模式：静态内部类实现 */ public class Singleton { // 私有构造方法：从根源上禁止外部代码直接 new Singleton() private Singleton() {} // 静态内部类：只有第一次被访问时才会加载（实现懒加载，用的时候才创建实例，不占内存） // 因为是在 Singleton 类内部，所以可以访问上面的 private 构造方法 private static class SingletonHolder { // 静态变量：JVM 类加载过程是天然线程安全的，这里初始化唯一的单例实例（不用加锁，性能高） private static final Singleton INSTANCE = new Singleton(); } // 全局唯一的访问点：外部代码只能通过这里获取单例实例 public static Singleton getInstance() { return SingletonHolder.INSTANCE; } public static void main(String[] args) { // 获取单例实例 Singleton instance1 = Singleton.getInstance(); Singleton instance2 = Singleton.getInstance(); // 两个实例是同一个，完美实现单例 System.out.println(instance1 == instance2); // 输出true } } 小贴士 静态变量存放在JVM的方法区（元空间），不是堆内存。 静态变量可以通过【类名.变量名】直接访问，也可以通过【实例.变量名】访问（不过，不推荐，因为容易混淆）。 因为所有线程共用同一个静态变量，多线程同时修改会出现并发问题，需要加锁保证线程安全。 因为实例变量和方法依赖具体的实例，静态方法调用时可能还没new对象，所以，静态方法只能直接访问静态变量和静态方法，不能直接访问实例变量和实例方法。 this 代表当前实例，静态方法属于类，调用时可能还没创建实例，this 根本不存在，所以，静态方法不能用 this。 静态内部类只能直接访问外部类的静态变量和静态方法，不能直接访问实例变量和实例方法。 ","date":"2026-03-22T13:06:05Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A3java%E7%9A%84static%E5%85%B3%E9%94%AE%E5%AD%97%E5%88%B0%E5%BA%95%E6%80%8E%E4%B9%88%E7%94%A8/","title":"【面试真题拆解】Java的Static关键字到底怎么用？"},{"content":"ThreadLocal 是线程专属的本地变量，它的设计是给每个线程创建一份独立的变量副本，线程之间完全不共享数据，天然实现线程隔离，全程无锁竞争，性能远高于加锁保证线程安全的方案。\n每个 Thread 内部都有一个专属的成员变量 ThreadLocalMap（不是 ThreadLocal 的！），Key 是 ThreadLocal 实例本身（弱引用），Value 是存的变量（强引用）。\n官话太多，直接上例子。\n以用户上下文传递举个例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 /** * 用户上下文工具类 * 用ThreadLocal存当前登录用户的ID，避免层层传参 */ public class UserContextHolder { // 定义ThreadLocal，存用户ID private static final ThreadLocal\u0026lt;Long\u0026gt; USER_ID_THREAD_LOCAL = new ThreadLocal\u0026lt;\u0026gt;(); /** * 设置用户ID */ public static void setUserId(Long userId) { USER_ID_THREAD_LOCAL.set(userId); } /** * 获取用户ID */ public static Long getUserId() { return USER_ID_THREAD_LOCAL.get(); } /** * 必须手动调用！清理ThreadLocal，防止内存泄漏 */ public static void clear() { USER_ID_THREAD_LOCAL.remove(); } } 和加锁保证线程安全的区别 对比项 加锁 ThreadLocal 思想 多线程排队用同一个变量，保证同一时间只有一个线程能访问 每个线程用自己的专属变量，完全不共享 性能 有锁竞争，高并发下性能差 无锁竞争，性能高 适用场景 多线程必须共享同一个变量的场景（比如共享计数器、共享资源修改） 每个线程需要独立变量副本的场景（比如用户上下文、SimpleDateFormat 线程安全） 小贴士 很多人以为ThreadLocalMap的Key是“线程ID”，其实Key是ThreadLocal实例本身，而不是线程ID。\n一个线程可以有多个ThreadLocal实例，每个实例对应一个Value，存到同一个ThreadLocalMap里，Key不同，互不干扰。\nThreadLocal内存泄漏问题 Java中的4种引用强度 强引用 我们平时写的 Object obj = new Object() 就是强引用，只要强引用还在，GC就不会回收这个对象。\n软引用 用 SoftReference 包装的引用，只有当内存不够用的时候，GC 才会回收这个对象。\n弱引用 用 WeakReference 包装的引用，只要GC一运行，不管内存够不够，都会回收这个对象。\n虚引用 用 PhantomReference 包装的引用，完全不影响对象的生命周期，只是用来在对象被 GC 回收前收到一个通知，做一些收尾工作。\nThreadLocalMap的Entry结构 ThreadLocalMap的Entry是继承自WeakReference的，结构如下：\n1 2 3 4 5 6 7 static class Entry extends WeakReference\u0026lt;ThreadLocal\u0026lt;?\u0026gt;\u0026gt; { Object value; Entry(ThreadLocal\u0026lt;?\u0026gt; k, Object v) { super(k); // Key是弱引用 value = v; // Value是强引用 } } 假设创建了一个 ThreadLocal 实例，有外部强引用指向它，同时把它作为 Key 存到了线程的 ThreadLocalMap 里；\n当用完 ThreadLocal 后，没有手动 remove，而且外部的强引用也没了（比如 threadLocal = null），因为 Key 是弱引用，GC 一运行，Key 就被回收了，变成了 null。\n但是， Value 是强引用，只要线程还活着，Value 就不会被 GC 回收，这就导致了内存泄漏。\n更严重的还有，如果用了线程池，线程会被长期复用，不仅内存泄漏，还会导致用户信息串号、数据错误等严重的业务问题。\n怎么避免内存泄漏 使用完ThreadLocal后，手动调用 remove() 方法。\n举个例子，在Spring Boot的拦截器里：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Component public class UserInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 从Token里解析出用户ID，存到ThreadLocal Long userId = parseUserIdFromToken(request.getHeader(\u0026#34;Authorization\u0026#34;)); UserContextHolder.setUserId(userId); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 在这里清理，不管请求成功失败，都要清理。 UserContextHolder.clear(); } } ","date":"2026-03-21T17:34:29Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A3%E4%BD%A0%E7%9F%A5%E9%81%93threadlocal%E6%98%AF%E4%BB%80%E4%B9%88%E5%90%97/","title":"【面试真题拆解】你知道ThreadLocal是什么吗"},{"content":"好几次面试都被问了计算机网络模型，一次是让我讲讲7层模型，还有让我讲讲5层模型，4层模型的。\n这群面试官完全不按套路出牌，昨天刚问完 7 层模型，今天就换成了 4 层模型。\n我当场脑子一片空白，支支吾吾说不清楚，直接就凉了。\n首先，为什么会有三个版本？\n且看：\n7层模型（OSI参考模型） 这是国际标准化组织定的理论学术标准，是网络分层的“祖宗”，但实际互联网几乎不用。\n4层模型（TCP/IP模型） 互联网实际在用的工业落地标准，我们现在上网、写代码用的HTTP、TCP、IP，全是基于这个模型跑的，是真正“干活”的模型。\n5层模型（教材简化模型） 国内计算机教材最常用的简化教学版，把4层模型的最底层拆成了2层，方便理解学习。\n面试的时候，不管面试官问哪个模型，可以先把下面这张对应关系表讲出来，这样就能把三个模型的边界划清楚，也不容易搞混。\n7 层 OSI 参考模型 5 层教材简化模型 4 层 TCP/IP 工业模型 作用 应用层 应用层 应用层 给用户/应用提供服务，比如浏览器、APP 表示层 （合并到应用层） （合并到应用层） 数据加密、格式转换、统一编码 会话层 （合并到应用层） （合并到应用层） 建立和维持应用之间的会话连接 传输层 传输层 传输层 给数据标端口，控制端到端的传输方式 网络层 网络层 网际层 给数据标 IP，规划端到端的传输路线 数据链路层 数据链路层 网络接口层 给数据标 MAC 地址，相邻节点之间的传输 物理层 物理层 （合并到网络接口层） 电信号、光信号的物理传输 7层OSI参考模型 7. 应用层 应用层主要是给用户/应用提供可操作的服务，定义数据的业务含义。\n统称为 消息/报文 (Message)。\n我们写代码天天接触的HTTP、HTTPS、DNS、FTP、WebSocket、RPC协议，全在这一层。\n浏览器发请求、APP调接口，都是在应用层完成的。\n6. 表示层 表示层主要是统一数据格式，保证收发双方能看懂对方的数据，核心负责数据的加密解密、格式转换、压缩解压、编码统一。\n理论上 OSI 模型把加密解密放在表示层；\n但在实际 TCP/IP 模型中，TLS/SSL 属于应用层实现，工作在应用层与传输层之间。\n5. 会话层 会话层主要是管理应用之间的会话，保证数据不会串线。\n负责建立、维持、有序断开应用之间的会话连接，提供会话同步、全双工 / 半双工切换等管理能力；\n该层仅存在于 OSI 理论模型中，实际 TCP/IP 体系中无独立对应层，相关会话逻辑由应用层 + 传输层共同实现。\n4. 传输层 传输层是端到端的传输控制，保证数据能准确送到对应的应用程序。\n数据单位称为 段 (Segment)。\nTCP（可靠传输）、UDP（不可靠传输），还有端口号（HTTP的80端口、HTTPS的443端口），都在这一层。\n3. 网络层 作用是网络寻址和路由选择，保证数据能从源主机送到目标主机。\n数据单位称为 包 (Packet) 或 分组。\nIP 协议（IPv4/IPv6）、ARP（IP 转 MAC 地址）、ICMP（ping/tracert 命令）、OSPF/RIP 路由协议，都在这一层。\n2. 数据链路层 相邻节点之间的可靠传输，负责成帧 (Framing)、差错检测和流量控制。\n数据单位称为 帧 (Frame)。\nMAC地址、以太网协议、交换机转发，都在这一层。\n给 IP 数据包加上 MAC 头部，标上源 MAC 和下一跳的 MAC 地址，保证相邻节点之间能准确传输。\n1. 物理层 物理介质上的比特流传输。只负责传输0和1的比特流，不管数据是什么含义。\n数据单位称为 比特 (Bit)。\n网线、光纤、网卡、集线器、电信号、光信号，都在这一层。\n4层TCP/IP模型 OSI模型太理想化、太复杂，实际互联网落地的时候，做了两层合并，就成了现在真正在用的4层模型：\n应用层：合并了OSI的应用层+表示层+会话层 传输层：同OSI的传输层 网际层：同OSI的网络层 网络接口层：合并了OSI的数据链路层+物理层 我们现在上网、写代码的所有网络请求，全是跑在这个4层TCP/IP模型上的，是真正的工业标准。\n5层教材简化模型 国内教材为了兼顾教学易懂性和底层逻辑完整性，以 OSI 7 层模型为基础做简化，保留应用层、传输层、网络层，同时将底层拆分为数据链路层 + 物理层，形成了教学通用的 5 层简化模型。\n这个模型既保留了底层的物理传输逻辑，又简化了上层的复杂分层。\n举个例子 在浏览器输入网址，到页面显示出来，经过了网络模型的哪些层？\n(1)客户端发送请求（从上到下封装数据包） 应用层 浏览器解析网址，先通过 DNS 协议解析域名，拿到服务器的 IP 地址，再生成对应的 HTTP 请求。\n传输层 先通过三次握手与服务器建立 TCP 连接，再给 HTTP 请求加上 TCP 头部，标上源端口和目标端口，完成传输层封装。\n网络层 给TCP数据包加上IP头部，标上源IP和目标IP，路由器根据IP地址规划路由，找到服务器的网络地址。\n数据链路层 给IP数据包加上MAC头部，标上源MAC和下一跳的MAC地址，交换机根据MAC地址完成相邻节点转发。\n物理层 把数据包转换成电信号/光信号，通过网线、光纤、基站等物理介质，传输到目标服务器。\n(2)服务器接收并处理请求（从下到上解析数据包） 服务器收到数据后，从下到上逐层解包：物理层➡️数据链路层➡️网络层➡️传输层➡️应用层，最终解析 HTTP 请求、执行业务逻辑并返回响应。\n(3) 客户端渲染页面 浏览器收到响应后，解析HTML/CSS/JS，渲染页面并显示给用户。\n每层对应的协议 层级名称 协议 应用层 HTTP、HTTPS、DNS、FTP、SMTP（邮件发送）、POP3/IMAP（邮件接收）、WebSocket、SSH、Dubbo 等 RPC 协议 传输层 TCP（可靠传输）、UDP（不可靠传输） 网络层 IPv4、IPv6、ARP（IP 转 MAC 地址）、ICMP（ping/tracert）、OSPF、RIP 等路由协议 数据链路层 以太网协议、PPP 点对点协议、VLAN 协议、MAC 地址规范 物理层 无专属网络协议，相关规范：网线、光纤、网卡、集线器、RJ45 接口 小贴士：\nARP 协议逻辑上属于网络层（核心为 IP 协议提供地址解析服务，是 TCP/IP 网际层的核心配套协议），仅从报文封装形式来看，它被直接封装在以太网帧中，因此也有观点认为它属于数据链路层。\n","date":"2026-03-21T09:04:13Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A3%E8%A2%AB%E9%97%AE%E6%87%B5%E7%9A%84%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C7%E5%B1%82_5%E5%B1%82_4%E5%B1%82%E6%A8%A1%E5%9E%8B/","title":"【面试真题拆解】被问懵的计算机网络7层_5层_4层模型"},{"content":"面试官问：\n做Web项目的时候，会暴露出一些HTTP接口，我现在想对它进行限流，限制5秒内只能访问10次，结合数据结构和算法，看如何实现，说说你的思路\n当时只说了个滑动窗口就没说其他的，然后就没有然后了。。。\n首先，为什么要限流？\n主要有以下三个原因：\n防止恶意刷接口，比如爬虫爬数据、黑客DDoS攻击。 保护服务器不被压垮，比如一个接口挂了，拖垮整个服务。 保证核心业务的可用性，比如大促场景下，支付接口优先，对普通接口进行限流。 技术选型 方案 数据结构+算法 核心思想 优缺点 适用场景 固定窗口计数器 数组/Map + 时间戳 把时间分成固定的窗口，每个窗口内计数 简单易实现； 有临界问题（比如4.9秒刷10次，5.1秒又刷10次，实际0.2秒内刷了20次） 对精度要求不高的场景 滑动窗口计数器 本地：LinkedList（双向链表）分布式：Redis ZSet（有序集合） + 时间戳 窗口是“滑动”的，只保留当前窗口内的请求 解决了固定窗口的临界问题；本地版 LinkedList 删头加尾 O (1)，分布式版 ZSet 按时间范围操作高效；本地版不适合分布式集群 固定时间窗口内精确计数的场景 漏桶算法 队列 + 定时器 像漏桶一样，请求先存到队列里，然后匀速处理 可以削峰填谷； 突发流量处理不了（秒杀时很多请求直接被拒） 对流量稳定性要求高的场景（比如消息队列） 令牌桶算法 队列 + 定时器 像令牌桶一样，匀速生成令牌，请求拿到令牌才能通过 可以削峰填谷，也 可以处理突发流量 对流量稳定性和突发流量都有要求的场景（比如API网关） 滑动窗口计数器 滑动窗口的核心，是窗口不再是静止分段的，而是随当前时间连续移动、互相重叠的：\n比如固定窗口，窗口是死的，0-5秒、5-10秒，边界固定不变。\n而滑动窗口，窗口的右边界永远是当前请求的时间，左边界永远是当前时间 - 窗口大小，窗口会跟着每一次请求一直往前滑。\n数据结构选型 本地：LinkedList（双向链表） 选择双向链表是因为我们需要频繁删除链表头部的过期时间戳，并且还需要频繁在链表尾部新增新的请求时间戳，而双向链表的删头、加尾操作时间复杂度都是O(1)，效率极高。\n分布式：Redis ZSet（有序集合） Web项目基本都是集群部署，本地计数器无法多实例同步，考虑用Redis实现。\n选择Redis中的ZSet数据结构，ZSet的score字段可以存请求的时间戳，天然支持按时间范围排序。\n实现逻辑 每次请求进来，执行 3 步：\n删掉所有早于当前时间 - 窗口大小的请求 统计当前窗口内的请求数，超过 10 次直接拒绝 把当前请求的时间戳加入数据结构，放行请求 小贴士 令牌桶的令牌生成速度要合理 不能太快（等于没限流），也不能太慢（很多请求被拒），要根据业务场景调整。\nRedis ZSet的过期时间要设置对 要设置成窗口大小+一点缓冲（比如1秒），防止数据提前过期，导致计数不准确。\n","date":"2026-03-20T15:00:00Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A35%E7%A7%92%E5%86%85%E9%99%9010%E6%AC%A1http%E6%8E%A5%E5%8F%A3%E8%AE%BF%E9%97%AE%E7%BB%93%E5%90%88%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E5%92%8C%E7%AE%97%E6%B3%95%E8%AF%B4%E8%AF%B4%E4%BD%A0%E7%9A%84%E6%80%9D%E8%B7%AF/","title":"【面试真题拆解】5秒内限10次HTTP接口访问，结合数据结构和算法说说你的思路"},{"content":"之前面试的时候还真的有一个面试官问过我：\n什么是事务？\n事务，说人话就是：\n把多个数据库操作打包成一个“不可分割的整体”，要么全成功，要么全失败并回滚到最初状态，绝对不会出现“只做了一半”的情况。\n以生活中最经典的银行转账举个例子那是再合适不过了。\n且看：\nA给B转100块钱，这一笔转账业务，其实是由两步独立的数据库操作组成的：\nA的账户扣100块 B的账户加100块 这两步必须同时成功、一起生效。\n如果A扣了钱B没收到，或者B收到了钱A没扣，那银行就乱套了。\n事务的四大特性 用银行转账的例子加深理解：\n特性 解释 银行转账例子 原子性（Atomicity） 要么全做，要么全不做，就像“原子”一样不可分割 A扣钱和B加钱必须同时成功，不能A扣了B没加 一致性（Consistency） 事务前后，数据要保持“一致”，总金额不变 转账前A有1000，B有0，总和是1000；转账后A有900，B有100，总和还是1000 隔离性（Isolation） 多个事务同时执行时，互相不干扰，就像“隔离开”一样 两个转账操作同时改A的账户，不能互相影响，不然钱就不对了 持久性（Durability） 事务提交后，数据就永久保存了，就算数据库挂了也不会丢 转账成功后，就算银行服务器断电重启，A和B的钱数也不会变 隔离级别 为什么要有隔离级别呢？\n如果多个事务同时执行，没有隔离的话，会出现3个问题：\n脏读 脏读就是读到了别人还没提交的数据。\n比如A转给B100块，还没提交，B就查到自己多了100块，结果A回滚了，B白高兴一场。\n不可重复读 不可重复读的意思是同一个事务里，两次读同一条数据的结果是不一样的。\n比如A查自己有1000块，中间B转给了A100块提交了，A再查就变成1100了。\n幻读 幻读就是同一个事务里，两次查的数量不一样。\n比如A查账户里有10条记录，中间B插了一条提交了，A再查就变成11条了，就像“幻觉”一样。\n这么看，不可重复读和幻读是由别的事务提交后导致的错误，而脏读是读到了别人还没提交的临时数据。\n那不可重复读和幻读两者的区别在哪呢？\n不可重复读是盯着一条记录看，发现这条记录的内容变了。\n而幻读是盯着一堆记录数看，发现总数量变了。\n类比一下就是不可重复读是你看自己的银行卡余额，钱数变了；而幻读是你数银行有多少张卡，卡的数量变了。\n隔离级别 解决的问题 解释 读未提交（Read Uncommitted） 啥都解决不了 能读到别人没提交的数据，脏读、不可重复读、幻读都有，一般不用 读已提交（Read Committed） 脏读 只能读到别人提交后的数据，但会有不可重复读、幻读 可重复读（Repeatable Read） 脏读、不可重复读 Spring默认的隔离级别。同一个事务里，两次读同一个数据一样，但会有幻读（MySQL InnoDB用MVCC解决了幻读） 串行化（Serializable） 所有问题 事务一个接一个执行，就像“排队”一样，性能最差，一般不用 传播行为 传播行为就是：\n当一个事务方法调用另一个事务方法时，这两个事务该怎么协作？是直接加入原事务？还是新建一个独立的事务？还是先挂起原事务，等新事务执行完再继续？\n以下是3 个常用的传播行为:\n传播行为 解释 适用场景 REQUIRED（默认） 有事务就加入，没有就新建一个事务 最常用，比如转账服务 REQUIRES_NEW 新建一个事务，挂起原事务，等新事务执行完再继续原事务 比如记录日志，不管主事务成功失败，日志都要记录 NESTED 嵌套事务，有一个保存点：1. 子事务回滚：只回滚自己，不影响父事务和其他子事务2. 父事务回滚：带着所有子事务一起回滚 系统批量发福利：某一个用户的福利发放失败，不影响其他用户；但整体回滚，所有用户的福利都会被收回 小贴士 Spring AOP 是基于代理的，@Transactional 只能用在 public 方法上，不然不生效。不管是 JDK 动态代理还是 CGLIB 代理，Spring 都只保证 public 方法的事务生效，非 public 的写了也白写。 内部调用不会走代理类，直接调用this.method()，所以，同一个类里方法调用，@Transactional不生效。 Spring默认只回滚非受检异常（RuntimeException、Error），受检异常（Exception）不回滚。如果要所有异常都回滚，需要在 @Transactional 里加rollbackFor = Exception.class。 MySQL的MyISAM引擎不支持事务，只有InnoDB支持。 ","date":"2026-03-20T09:06:26Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A3spring%E4%BA%8B%E5%8A%A1%E6%9C%BA%E5%88%B6/","title":"【面试真题拆解】Spring事务机制"},{"content":"我的个人项目中有这么一个功能，就是个人作品页面需要显示详情页的预览截图，这样会显得更专业一点。\n但是，截图之前是存在腾讯云COS中的，最近我的腾讯云 COS 到期了，我就开始寻找替代方案。\n想换 Cloudflare R2 吧，听说很香零费用，结果还要信用卡验证，我这种没有国际信用卡的人直接被拒之门外。\n经过一番调研和实践，我锁定了一个纯白嫖、零成本、高速度的方案：\nGitHub 仓库存储 + jsDelivr CDN 加速\n本文将介绍一下如何在 Spring Boot 项目中集成这一方案。\n技术选型 维度 腾讯云 COS GitHub + jsDelivr 存储费用 按量计费/资源包（需续费） 完全免费 (建议单仓库 \u0026lt; 1GB，单文件 \u0026lt; 50MB，GitHub 有软限制) 流量费用 下行流量贵（容易被盗链刷爆） 完全免费 (jsDelivr 提供的全球加速) 支付门槛 需实名、绑定支付方式 仅需一个 GitHub 账号 国内访问 速度快，但费钱 通过 jsDelivr 加速，速度极快 准备工作 创建一个新的公开仓库（Public），例如 ObjectStorage。 生成 Personal Access Token (PAT) 在Github账户进入【Settings】➡️【Developer settings】➡️【Personal access tokens】➡️【 Tokens (classic)】。\n小贴士\n创建Token时必须要勾选 repo 权限，并且创建的时候就得保存好生成的 Token，因为它只会出现一次。\n图片截图上传成功后，访问链接格式为：\nhttps://cdn.jsdelivr.net/gh/用户名/仓库名@分支名/文件路径\n代码实现 引入依赖 需要 OkHttp 处理网络请求，FastJSON 处理 JSON 数据。\n1 2 3 4 5 6 7 8 9 10 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.squareup.okhttp3\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;okhttp\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.10.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;fastjson\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.0.32\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; application-local 为了开源安全，将敏感信息放在 application-local.yml 中，并确保该文件在 .gitignore 中。\n1 2 3 4 5 6 # application-local.yml github: owner: github用户名 repo: ObjectStorage branch: main token: ghp_你的TOKEN GithubConfig 讲原CosConfig加上@Deprecated，新增GithubConfig\n1 2 3 4 5 6 7 8 9 10 11 12 13 import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @Data @Configuration @ConfigurationProperties(prefix = \u0026#34;github\u0026#34;) public class GithubConfig { private String owner; private String repo; private String branch; private String token; } GithubManager 为了模仿对象存储的 SDK，将文件转为 Base64 并调用 GitHub API 进行 PUT 上传。\n将原CosManager加上@Deprecated，新增GithubManager\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 @Component @Slf4j public class GithubManager { @Resource private GithubConfig githubConfig; private final OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .build(); /** * 上传文件到 GitHub 仓库 * * @param key 存储路径 * @param file 本地文件 * @return jsDelivr CDN 访问地址 */ public String uploadFile(String key, File file) { // 1. 读取文件并转为 Base64 byte[] fileBytes = FileUtil.readBytes(file); String base64Content = Base64.getEncoder().encodeToString(fileBytes); // 2. 构建 GitHub API 请求体 JSONObject json = new JSONObject(); json.put(\u0026#34;message\u0026#34;, \u0026#34;upload screenshot: \u0026#34; + file.getName()); json.put(\u0026#34;content\u0026#34;, base64Content); json.put(\u0026#34;branch\u0026#34;, githubConfig.getBranch()); // 注意：GitHub API 路径开头不能有 / if (key.startsWith(\u0026#34;/\u0026#34;)) { key = key.substring(1); } String url = String.format(\u0026#34;https://api.github.com/repos/%s/%s/contents/%s\u0026#34;, githubConfig.getOwner(), githubConfig.getRepo(), key); RequestBody body = RequestBody.create( json.toJSONString(), MediaType.parse(\u0026#34;application/json; charset=utf-8\u0026#34;) ); Request request = new Request.Builder() .url(url) .addHeader(\u0026#34;Authorization\u0026#34;, \u0026#34;token \u0026#34; + githubConfig.getToken()) .put(body) .build(); // 3. 发送请求 try (Response response = client.newCall(request).execute()) { if (response.isSuccessful()) { // 返回 jsDelivr CDN 链接 return String.format(\u0026#34;https://cdn.jsdelivr.net/gh/%s/%s@%s/%s\u0026#34;, githubConfig.getOwner(), githubConfig.getRepo(), githubConfig.getBranch(), key); } else { log.error(\u0026#34;GitHub 上传失败, 错误码: {}, 详情: {}\u0026#34;, response.code(), response.body().string()); return null; } } catch (IOException e) { log.error(\u0026#34;GitHub 上传网络异常\u0026#34;, e); return null; } } } GithubScreenshotServiceImpl 为了保留原ScreenshotServiceImpl的逻辑，新增一个GithubScreenshotServiceImpl同样也实现ScreenshotService 接口。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 /** * GithubScreenshotServiceImpl类实现了ScreenshotService接口，提供网页截图生成并上传到 GitHub 的服务 */ @Service(\u0026#34;githubScreenshotService\u0026#34;) // 指定 Bean 名称，防止和原 COS 实现冲突 @Slf4j public class GithubScreenshotServiceImpl implements ScreenshotService { @Resource private GithubManager githubManager; // 注入新写的 GithubManager @Override public String generateAndUploadScreenshot(String webUrl) { ThrowUtils.throwIf(StrUtil.isBlank(webUrl), ErrorCode.PARAMS_ERROR, \u0026#34;网页URL不能为空\u0026#34;); log.info(\u0026#34;开始生成网页截图(GitHub方案)，URL:{}\u0026#34;, webUrl); // 1. 调用工具方法保存网页截图到本地 String localScreenshotPath = WebScreenshotUtils.saveWebPageScreenshot(webUrl); ThrowUtils.throwIf(StrUtil.isBlank(localScreenshotPath), ErrorCode.OPERATION_ERROR, \u0026#34;本地截图生成失败\u0026#34;); try { // 2. 调用方法将本地截图上传到 GitHub String githubUrl = uploadScreenshotToGithub(localScreenshotPath); ThrowUtils.throwIf(StrUtil.isBlank(githubUrl), ErrorCode.OPERATION_ERROR, \u0026#34;截图上传 GitHub 失败\u0026#34;); log.info(\u0026#34;网页截图生成并上传 GitHub 成功: {} -\u0026gt; {}\u0026#34;, webUrl, githubUrl); return githubUrl; } finally { // 3. 清理本地文件 cleanupLocalFile(localScreenshotPath); } } private String uploadScreenshotToGithub(String localScreenshotPath) { if (StrUtil.isBlank(localScreenshotPath)) { return null; } File screenshotFile = new File(localScreenshotPath); if (!screenshotFile.exists()) { log.error(\u0026#34;截图文件不存在: {}\u0026#34;, localScreenshotPath); return null; } // 生成文件名 String fileName = UUID.randomUUID().toString().substring(0, 8) + \u0026#34;_compressed.jpg\u0026#34;; // 生成 GitHub 存储路径 String githubKey = generateScreenshotKey(fileName); // 调用 GitHub 管理器 return githubManager.uploadFile(githubKey, screenshotFile); } private String generateScreenshotKey(String fileName) { String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern(\u0026#34;yyyy/MM/dd\u0026#34;)); // 注意：GitHub 路径不建议以 / 开头 return String.format(\u0026#34;screenshots/%s/%s\u0026#34;, datePath, fileName); } private void cleanupLocalFile(String localFilePath) { File localFile = new File(localFilePath); if (localFile.exists()) { File parentDir = localFile.getParentFile(); FileUtil.del(parentDir); log.info(\u0026#34;本地截图临时文件已清理: {}\u0026#34;, localFilePath); } } } 小贴士\n这里有一个问题需要注意，@Deprecated 只是给程序员看的“警告”标签，它并不能阻止 Spring 扫描并加载这个 Bean。\n所以，为了避免Spring 在启动时发现有两个类都实现了 ScreenshotService 接口，并且都加上了 @Service 注解。\n当其他地方需要注入 ScreenshotService 时，Spring 不知道该选哪一个的问题。\n需要在ScreenshotServiceImpl类中添加@Deprecated 的同时，把@Service 注解注释掉。\n1 2 3 4 5 6 // @Service \u0026lt;-- 把这个注释掉，Spring 就不会把它当成一个 Bean 加载了 @Slf4j @Deprecated public class ScreenshotServiceImpl implements ScreenshotService { // ... 原有的代码 } 之后项目的预览界面就可以看到截图了：\n","date":"2026-03-19T17:10:48Z","permalink":"https://iamxurulin.github.io/p/%E5%91%8A%E5%88%AB%E8%85%BE%E8%AE%AF%E4%BA%91-cos%E7%94%A8-github--jsdelivr-%E6%90%AD%E5%BB%BA%E9%9B%B6%E6%88%90%E6%9C%AC%E5%9B%BE%E5%BA%8A/","title":"告别腾讯云 COS，用 GitHub + jsDelivr 搭建零成本图床"},{"content":"设计模式其实就是前辈们写代码踩了无数坑，总结出来的代码编写最佳实践，专门用来解决特定场景下的代码复用、解耦、扩展性问题。\n可以分为创建型模式、结构型模式、行为型模式三类。\n分类 核心作用 包含的设计模式 数量 创建型模式 解决对象怎么创建的问题，解耦对象的创建和使用 单例、工厂方法、抽象工厂、建造者、原型 5种 结构型模式 解决类/对象怎么组合的问题，不修改原有代码实现功能扩展 适配器、桥接、组合、装饰、外观、享元、代理 7种 行为型模式 解决类/对象之间怎么交互、职责怎么分配的问题，实现灵活的通信和流程控制 模板方法、命令、迭代器、观察者、中介者、备忘录、解释器、状态、策略、职责链、访问者 11种 记忆口诀 创建型口诀（5种） 单身汉在工厂抽象建原型。\n一个单身汉，在工厂里用抽象方法，建造了一个原型。\n结构型口诀（7种） 桥上代理卖装饰，组合适配享外观。\n桥上的代理商在卖装饰品，把零件组合适配好，让你享受精美外观。\n行为型口诀（11种） 访客观察定策略，模板迭代改状态，备忘命令交中介，解释责任一条链。\n访客观察后定下策略，按模板迭代改变状态，把备忘录和命令交给中介，出了问题沿责任链逐一解释。\n创建型模式 不让你自己new对象，把对象创建的逻辑封装起来，和业务代码解耦。\n1. 单例模式 整个系统里，这个类有且只有一个实例对象，不管你在哪获取，拿到的都是同一个东西。\n类比 就像公司里只有一个 CEO，不管哪个部门找，对接的都是同一个人。\n适用场景 Spring容器里的单例Bean、全局配置类、日志对象\n代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Singleton { // 1. 私有构造方法：禁止外部new对象 private Singleton() {} // 2. 私有静态实例，volatile禁止指令重排 private static volatile Singleton instance; // 3. 公共静态方法，获取唯一实例 public static Singleton getInstance() { // 第一次校验：实例已存在就直接返回，不用抢锁 if (instance == null) { // 加锁，保证线程安全 synchronized (Singleton.class) { // 第二次校验：防止多线程同时进入第一个if，重复创建 if (instance == null) { instance = new Singleton(); } } } return instance; } } 2. 工厂方法模式 定义一个创建对象的工厂接口，让子类决定创建哪个对象，把对象的创建延迟到子类。\n类比 就像奶茶店是工厂接口，蜜雪冰城、喜茶是子类工厂，你去哪个店，就喝哪个店的奶茶，不用管奶茶是怎么做的。\n适用场景 不知道要创建的对象具体类型，交给子类决定 把对象创建和业务代码解耦 Spring的FactoryBean 代码示例 1 2 3 4 5 6 7 8 9 10 11 12 // 1. 产品接口：奶茶 public interface MilkTea { void make(); } // 2. 具体产品：蜜雪冰城奶茶 public class MixueMilkTea implements MilkTea { @Override public void make() { System.out.println(\u0026#34;蜜雪冰城奶茶做好了！\u0026#34;); } } 3. 抽象工厂模式 工厂方法的升级版，一个工厂可以创建一系列相关的产品，而不是只创建一个。\n和工厂方法的区别是：\n工厂方法是单个产品，而抽象工厂是一套相关产品。\n类比 就像蜜雪冰城不仅能做奶茶，还能做冰淇淋、圣代，一个工厂搞定同品牌的一系列产品。\n适用于需要创建一系列相关联的产品，而不是单个产品的场景（比如同一品牌的奶茶 + 冰淇淋 + 圣代）。\n4. 建造者模式 把一个复杂对象的创建和表示分离，一步一步构建出复杂对象，不用管内部组装细节。\n类比 就像组装电脑，你只需要告诉装机店你要什么 CPU、显卡、内存，装机店帮你一步步组装好，不用你自己焊主板。\n适用场景 复杂对象的创建，参数特别多（比如超过4个参数） Lombok的@Builder注解 代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 // 电脑类（复杂对象） public class Computer { private String cpu; private String gpu; private int memory; private int storage; // 私有构造方法，只能通过Builder创建 private Computer(Builder builder) { this.cpu = builder.cpu; this.gpu = builder.gpu; this.memory = builder.memory; this.storage = builder.storage; } // 建造者静态内部类 public static class Builder { private String cpu; private String gpu; private int memory; private int storage; // 链式调用设置参数 public Builder cpu(String cpu) { this.cpu = cpu; return this; } public Builder gpu(String gpu) { this.gpu = gpu; return this; } public Builder memory(int memory) { this.memory = memory; return this; } public Builder storage(int storage) { this.storage = storage; return this; } // 构建最终对象 public Computer build() { return new Computer(this); } } } // 测试 public class Test { public static void main(String[] args) { // 链式调用，一步一步构建对象 Computer computer = new Computer.Builder() .cpu(\u0026#34;i7-14700K\u0026#34;) .gpu(\u0026#34;RTX4070\u0026#34;) .memory(32) .storage(1024) .build(); } } 5. 原型模式 基于一个已有的对象，克隆出一个一模一样的新对象，不用重新new和初始化。\n类比 就像打印机复印文件，不用重新手写一遍，直接复印一份一模一样的。\n适用场景 对象创建成本很高（比如初始化要查数据库、调接口） 想要和原对象一模一样的新对象，又不想重新初始化 Spring的Bean的原型作用域 结构型模式 不修改原有代码，通过类/对象的组合，实现功能扩展和灵活组装。\n1. 适配器模式 把一个不兼容的接口，转换成你想要的兼容接口，让两个不搭边的类能一起工作。\n类比 就像 Type-C 转 3.5mm 耳机转接头，手机只有 Type-C 口，耳机是 3.5mm 口，转接头让它们能一起用。\n适用场景 老系统接口复用，适配新的业务 Spring MVC的HandlerAdapter 2. 装饰器模式 在不修改原有类的基础上，动态给对象添加新功能，比继承更灵活。\n类比 就像买奶茶，基础款是纯茶，你可以加珍珠、加椰果、加奶盖，一层一层加功能，不用重新做一杯新奶茶。\n适用场景 动态给对象添加/撤销功能，不想用继承搞出一堆子类 Java IO流（FileInputStream + BufferedInputStream） Spring的TransactionProxyFactoryBean 3. 代理模式 给目标对象创建一个代理对象，通过代理对象控制对目标对象的访问，还能在不修改目标对象的前提下加额外功能。\n类比 就像租房找中介，你不用直接对接房东，中介帮你搞定看房、签合同，还能帮你做背景调查。\n适用场景 远程代理：RPC调用 权限控制、日志记录、事务管理 Spring AOP的动态代理 4. 外观模式 给复杂的子系统提供一个统一的高层入口，隐藏子系统的复杂细节，调用方只需要和这个入口交互就行。\n类比 就像酒店前台，你不用管酒店的保洁、维修、餐饮、安保各个部门，有任何事找前台就行，前台帮你对接所有部门。\n适用场景 复杂系统简化调用，封装底层细节 Spring Boot的starter就是外观模式的思想 5. 桥接模式 把抽象和实现分离，让它们可以独立变化，解决多维度变化导致的类爆炸问题。\n类比 就像手机和手机壳，手机有苹果、华为、小米，手机壳有透明、硅胶、卡通，两个维度独立变化，不用为每个手机做一套专属手机壳。\n适用于类有多个独立变化的维度，用继承会导致子类数量爆炸的场景。\n6. 组合模式 把对象组合成树形结构，用来表示整体-部分的层级关系，让调用方对单个对象和组合对象的使用是一致的。\n类比 就像公司组织架构，总公司下面有分公司，分公司下面有部门，部门下面有员工，不管是总公司、分公司、部门还是员工，都可以统一用组织节点来表示。\n适用场景 树形结构的数据，比如菜单、部门组织、文件目录 想要统一处理单个对象和组合对象，不用区分是整体还是部分 7. 享元模式 共享细粒度的对象，减少对象创建的数量，节省内存，核心是复用对象。\n类比 就像共享单车，不是每个人都买一辆新自行车，而是大家共享一批单车，用完还回去，别人继续用。\n适用场景 Java的字符串常量池、Integer缓存池 连接池、线程池（享元模式 + 单例模式的结合体） 行为型模式 解决类和对象之间的通信、职责分配问题，让对象之间协作更灵活、职责更清晰。\n1. 策略模式 定义一系列算法，把每个算法封装起来，让它们可以互相替换，算法的变化不会影响使用算法的用户。\n类比 就像出行，你可以选骑自行车、坐地铁、开车、坐飞机，这些都是不同的出行策略，你可以根据目的地、预算随便换，不用改变你要出门的目标。\n适用场景 一个业务有多种实现方式，需要动态切换 想要避免大量的if-else/switch分支 Spring的Resource接口、BeanNameGenerator 代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 // 1. 策略接口：出行方式 public interface TravelStrategy { void travel(); } // 2. 具体策略：坐地铁 public class SubwayStrategy implements TravelStrategy { @Override public void travel() { System.out.println(\u0026#34;坐地铁出行，便宜又快\u0026#34;); } } // 3. 具体策略：开车 public class CarStrategy implements TravelStrategy { @Override public void travel() { System.out.println(\u0026#34;开车出行，自由方便\u0026#34;); } } // 4. 用户 public class Traveler { private TravelStrategy strategy; // 动态设置策略 public void setStrategy(TravelStrategy strategy) { this.strategy = strategy; } // 执行出行 public void goTravel() { strategy.travel(); } } // 测试 public class Test { public static void main(String[] args) { Traveler traveler = new Traveler(); // 今天坐地铁 traveler.setStrategy(new SubwayStrategy()); traveler.goTravel(); // 明天开车 traveler.setStrategy(new CarStrategy()); traveler.goTravel(); } } 2. 观察者模式 定义了对象之间一对多的依赖关系，当一个对象的状态发生变化时，所有依赖它的对象都会收到通知并自动更新。\n类比 就像你关注了一个CSDN博主，博主更新博客了，所有关注他的粉丝都会收到推送通知。\n适用场景 事件驱动、消息通知、发布订阅 Spring的ApplicationEvent事件机制 Java的EventListener 3. 模板方法模式 定义一个操作的算法骨架，把其中一些步骤延迟到子类实现，子类可以不改变算法骨架，就可以重定义特定步骤。\n类比 就像煮奶茶，固定骨架是煮茶➡️加奶➡️加料➡️装杯，不同的奶茶只是加料、煮茶时间不一样，骨架永远不变。\n小贴士 煮奶茶是模板方法模式，关注的是怎么做奶茶，而开奶茶店造奶茶是工厂方法模式，关注的是造什么奶茶。\n适用场景 多个子类有公共的流程，只有个别步骤不同 Spring的JdbcTemplate、RedisTemplate Spring MVC的HttpServlet 4. 责任链模式 把请求的发送者和接收者解耦，让多个对象都有机会处理这个请求，把这些对象连成一条链，请求沿着链传递，直到有对象处理它为止。\n类比 就像公司请假审批，1天以内组长批，3天以内经理批，7天以内总监批，超过7天老板批，你的请假申请沿着这个审批链传递，直到有人处理。\n适用场景 一个请求需要多个对象依次处理 Spring MVC的HandlerInterceptor拦截器链 Servlet的Filter过滤器链 5. 状态模式 当一个对象的内部状态发生变化时，改变它的行为，看起来就像这个对象换了一个类。\n类比 就像电梯，开门状态下只能关门，不能上下行；运行状态下只能停梯，不能开门；不同的状态，电梯的行为完全不一样。\n适用场景 对象的行为由状态决定，运行时状态变化会改变行为 代码里有大量和状态相关的if-else/switch分支 小贴士 状态模式和策略模式容易混淆，不过两者的区别是：\n状态模式是状态决定行为，对象的行为由当前状态决定；而策略模式是算法可替换，封装一系列平级算法，想换哪个换哪个。 状态模式有明确的状态流转图，状态之间不能随便跳，必须按规则流转；策略模式没有流转关系，策略之间是平级的，想换就换。 状态模式关注的是对象状态的变化以及状态变化带来的行为变化；策略模式关注的是算法的选择和替换，不关心算法之间的关系。 扯了那么多官话，好像没多大用，举个例子套一下大概就是：\n比如电梯是状态模式，开门、上下行、关门有明确的流转规则；\n而出行是策略模式，骑自行车、坐地铁、开车是平级的，想换就换。\n6. 命令模式 把一个请求封装成一个命令对象，让你可以用不同的请求参数化对象，支持请求排队、撤销、重做。\n类比 就像餐厅点餐，你点的菜就是一个命令，服务员把命令传给后厨，后厨执行，你不用管后厨是谁、怎么做的，还能随时加菜、退菜。\n适用场景 Spring 的 JmsTemplate（JMS 消息队列）和 RabbitTemplate（RabbitMQ 消息队列） 7. 迭代器模式 提供一种统一的方式，遍历一个集合对象里的所有元素，不用暴露集合的内部结构。\n类比 就像景区观光车，不管景区里的景点是怎么分布的，你坐观光车就能一个一个逛完所有景点，不用管景区内部的路线设计。\n适用场景 Java的Iterator接口、增强for循环 Spring的CompositeIterator 8. 中介者模式 用一个中介对象封装一系列对象的交互，让对象之间不用直接互相引用，解耦对象之间的复杂依赖，只和中介者交互。\n类比 就像机场塔台，所有飞机不用互相沟通，只需要和塔台沟通，塔台统一调度，避免撞机。\n适用场景 想要解耦多个对象的交互，减少耦合 Spring MVC的DispatcherServlet，就是前端控制器中介者 9. 备忘录模式 在不破坏封装的前提下，捕获一个对象的内部状态，并保存下来，之后可以恢复到这个状态。\n类比 就像游戏存档，你打BOSS之前存个档，打输了可以读档回到存档时的状态，不用重新从头玩。\n适用场景 撤销、回滚操作 数据库的事务回滚 IDE的撤销操作 10. 访问者模式 在不修改数据结构的前提下，定义对数据结构中元素的新操作，把数据结构和操作分离。\n类比 比如电商订单系统，数据结构是固定的订单和订单明细，操作是经常变化的计算总价、打印发票、生成物流单、发送短信通知。不用修改订单 / 订单明细的代码，只要新增一个访问者（比如生成电子发票），就能给订单增加新操作。\n适用于想要给数据结构里的元素增加新操作，又不想修改数据结构的场景。\n11. 解释器模式 给定一个语言，定义它的语法规则，然后定义一个解释器，用这个解释器来解释语言中的句子。\n类比 就像翻译官，你说中文，翻译官根据中英文的语法规则，把你的话翻译成英文。\n适用场景 SQL解析、正则表达式解析 Spring的SpEL表达式 ","date":"2026-03-19T12:50:01Z","permalink":"https://iamxurulin.github.io/p/23%E7%A7%8D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B8%80%E6%AC%A1%E6%80%A7%E8%AE%B2%E6%98%8E%E7%99%BD/","title":"23种设计模式，一次性讲明白"},{"content":"很多面试官早就对“Redis缓存、MySQL分库分表”这些东西形成免疫了，他们更想要听到的是一个完整的链路，比如从服务到数据、从接入到落地的全流程设计。\n当时面试官问我：\n一家电商公司，它主要是面向消费者的，公司的一些业务场景流量会很高，比如说展示商品的那个页面，它的流量就很高，假设现在由你来做这个系统，负责商品详情页的展示，你能从服务的层面，数据的层面等多个维度来设计一下，让它能够支持一个更高的查询能力。\n场景梳理 业务背景\n电商商品详情页，面对百万级QPS的高并发流量（比如大促、爆款商品）。\n需求分析\n性能：支持高并发查询，响应时间 \u0026lt; 100ms 一致性：商品价格、库存、标题修改后，能及时更新 高可用：服务挂了、Redis挂了，系统不崩 可扩展：新增商品、新增规格，能快速接入 关键数据\n商品基本信息（标题、主图、详情图）、价格（实时/活动）、库存、规格参数、促销信息（优惠券/秒杀）、评价统计。\n设计思路 以“多级缓存”为核心，配合“服务分层+数据分层+异步更新”，构建“读多写少”的高并发详情页系统。\n系统架构\n服务层面 采用前后端分离+网关层+服务分层架构：\n层级 作用 CDN层 静态资源加速（图片、js、css、详情页HTML） 网关层 路由、限流、鉴权、接口聚合 API网关聚合层 聚合多个微服务接口（比如商品基础信息、价格、库存、评价），减少请求次数 商品服务层 核心业务逻辑（查商品、查库存、查价格） 数据服务层 专门做数据查询（缓存+数据库） 商品详情页是典型的读多写少场景，考虑服务层本地缓存 + 分布式缓存。\n采用Caffeine作为服务层本地缓存，缓存极热点商品（比如爆款商品），避免每次请求都走Redis，\n采用Redis作为分布式缓存，作为核心缓存层，缓存所有商品的详情数据。\n调用流程：\n先查本地缓存（Caffeine），如果命中就直接返回；\n如果本地未命中，查 Redis，命中则写入本地缓存，返回；\n如果Redis 未命中，查数据库，查到则写入 Redis + 本地缓存，返回。\n数据层面 商品数据分为3类，不同类型用不同策略：\n数据类型 例子 缓存策略 静态数据 商品标题、主图、详情图、规格参数 永久缓存（不过期），更新时主动删除 半动态数据 商品价格、促销信息（优惠券、满减） 短期缓存（5-10分钟），更新时主动删除 动态数据 库存（实时）、实时销量 单独缓存，库存用Redis原子操作；实时销量异步更新 缓存更新的原则：\n为了避免高并发下的缓存不一致问题，采用的是先更新数据库，再删除缓存的方式。\n高并发优化 1.热点数据预热\n大促前，把热门商品（如销量TOP100）写入Redis和本地缓存。\n限流+熔断+降级 对商品详情接口设置限流。\n考虑使用服务熔断，当Redis/数据库不可用时，快速失败，避免系统雪崩。\n或者考虑使用降级策略，当Redis挂了，降级到直接查数据库，如果数据库也挂了，降级返回默认商品信息。\n页面静态化 对于完全静态的商品（比如虚拟商品或者不参与促销的商品），生成静态HTML页面，直接存到CDN，这样的话用户请求直接由CDN返回，不经过任何服务。\n","date":"2026-03-18T12:32:01Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A3%E7%94%B5%E5%95%86%E9%AB%98%E5%B9%B6%E5%8F%91%E5%9C%BA%E6%99%AF%E4%B8%8B%E5%95%86%E5%93%81%E8%AF%A6%E6%83%85%E9%A1%B5%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/","title":"【面试真题拆解】电商高并发场景下商品详情页系统设计"},{"content":"这是面试遇到的一道场景题。\n面试官问我：\n有这么一个场景，现在有一个网站，这个网站访问的人非常多，在登录的时候有些坏人来这里捣乱，怎么去设计一个系统实现把这些坏人找到的逻辑。\n条件说一下：\n登录的时候你要的用户userid，时间 ，设备deviceid这些信息都有；\n坏人的定义是：最近十分钟之内，一个设备上登录了大于等于5个user，我就认为这个是坏人。\n场景梳理 输入信息：用户登录时的 userId（用户ID）、timestamp（登录时间戳）、deviceId（设备ID）； 坏人定义：最近10分钟（600秒）内，同一个deviceId上登录了≥5个不同的userId； 输出结果：标记该deviceId为“恶意设备”，后续可以做拦截、验证码、风控等处理。 实现方案 考虑用Redis ZSet实现滑动窗口。\n为什么选Redis？ 主要是因为：\n单线程高性能，适合高并发场景\n内存数据库，读写速度极快\n支持丰富的数据结构（比如这次场景需要的ZSet）\n支持过期时间，自动清理数据\n为什么选ZSet？ ZSet（有序集合）是 Redis 中最适合做 “时间窗口、排行榜、去重 + 排序” 的数据结构。\n它有以下几个特点：\nMember（成员）唯一，Member 不能重复，天然去重；\n每个 Member 对应一个 Score（分数），Score 是一个浮点数，用来排序；\nZSet 会自动按 Score 从小到大进行排序，如果Score 相同则按 Member 字典序排序。\nZSet 的底层是哈希表 + 跳表，哈希表保证 Member 唯一、O (1) 查找，跳表保证有序、O (log n) 范围查询。\n数据模型 维度 说明 Key login:device:{deviceId}（比如 login:device:abc123） Value（ZSet Member） userId（用户ID，保证唯一性，去重） Score（ZSet Score） 登录时间戳（用于滑动窗口） 理一下整个流程 用户发起登录请求：携带 userId、deviceId、timestamp； 先查黑名单：如果已经是恶意设备，直接拦截； 写入登录记录到Redis ZSet：以 deviceId 为Key，userId 为Member，timestamp 为Score，写入ZSet； 清理过期数据：删除ZSet中Score \u0026lt; 当前时间 - 10分钟的元素； 统计最近10分钟内的不同userId数量：用 ZCard 命令统计ZSet的元素个数； 判断是否为坏人：如果数量≥5，标记该deviceId为恶意设备，存入Redis黑名单； 返回登录结果：如果是恶意设备，返回“请完成验证码”或“登录失败”；否则正常登录。 简化版的代码实现 用Spring Boot + RedisTemplate写一个简化版的实现，加深理解一下：\n（1）先定义Redis Key的前缀和常量 1 2 3 4 5 6 7 8 9 10 11 12 public class RedisKeyConstants { // 设备登录记录Key前缀 public static final String LOGIN_DEVICE_KEY_PREFIX = \u0026#34;login:device:\u0026#34;; // 恶意设备黑名单Key前缀 public static final String MALICIOUS_DEVICE_KEY_PREFIX = \u0026#34;malicious:device:\u0026#34;; // 滑动窗口大小：10分钟（600秒） public static final int WINDOW_SIZE_SECONDS = 600; // 坏人阈值：≥5个不同userId public static final int MALICIOUS_THRESHOLD = 5; // 过期时间：10分钟 public static final int EXPIRE_TIME_SECONDS = 600; } （2）服务实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service public class LoginRiskControlService { @Autowired private RedisTemplate\u0026lt;String, String\u0026gt; redisTemplate; /** * 检查是否为恶意设备 * @param userId 用户ID * @param deviceId 设备ID * @return true-是恶意设备，false-正常设备 */ public boolean checkMaliciousDevice(String userId, String deviceId) { // 1. 构造Redis Key String loginKey = RedisKeyConstants.LOGIN_DEVICE_KEY_PREFIX + deviceId; String maliciousKey = RedisKeyConstants.MALICIOUS_DEVICE_KEY_PREFIX + deviceId; // 2. 先查黑名单：如果已经是恶意设备，直接返回true if (Boolean.TRUE.equals(redisTemplate.hasKey(maliciousKey))) { return true; } // 3. 获取当前时间戳（秒） long currentTime = System.currentTimeMillis() / 1000; // 计算窗口起始时间：当前时间 - 10分钟 long windowStartTime = currentTime - RedisKeyConstants.WINDOW_SIZE_SECONDS; // 4. 写入当前登录记录到ZSet：Member=userId，Score=当前时间戳 redisTemplate.opsForZSet().add(loginKey, userId, currentTime); // 5. 清理过期数据：删除Score \u0026lt; 窗口起始时间的元素 redisTemplate.opsForZSet().removeRangeByScore(loginKey, 0, windowStartTime); // 6. 统计最近10分钟内的不同userId数量（ZSet的元素个数） Long count = redisTemplate.opsForZSet().zCard(loginKey); // 7. 设置Key的过期时间：10分钟（防止内存泄漏） redisTemplate.expire(loginKey, RedisKeyConstants.EXPIRE_TIME_SECONDS, TimeUnit.SECONDS); // 8. 判断是否为坏人：数量≥5 if (count != null \u0026amp;\u0026amp; count \u0026gt;= RedisKeyConstants.MALICIOUS_THRESHOLD) { // 标记为恶意设备，存入黑名单，过期时间可以设长一点（比如24小时） redisTemplate.opsForValue().set(maliciousKey, \u0026#34;1\u0026#34;, 24, TimeUnit.HOURS); return true; } // 9. 正常设备，返回false return false; } } （3）在登录接口中调用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(\u0026#34;/login\u0026#34;) public class LoginController { @Autowired private LoginRiskControlService riskControlService; @PostMapping public String login(String userId, String deviceId) { // 1. 先检查是否为恶意设备 boolean isMalicious = riskControlService.checkMaliciousDevice(userId, deviceId); if (isMalicious) { return \u0026#34;登录失败：检测到异常设备，请完成验证码验证\u0026#34;; } // 2. 正常登录逻辑（校验账号密码、生成Token等） // ... return \u0026#34;登录成功\u0026#34;; } } 高并发场景的优化 在访问量非常大的场景下，上面的代码肯定扛不住，所以还需要做一些优化。\n1. 布隆过滤器快速过滤正常设备 如果每个登录请求都查Redis，Redis的压力会很大。\n可以考虑用布隆过滤器先快速过滤：\n在布隆过滤器里只存“潜在恶意设备”（比如最近10分钟内登录过≥3个userId的设备），登录时先查布隆过滤器，如果不在里面，直接放行，不查Redis；如果在里面，再查Redis确认。\n2. 异步处理 如果Redis查询慢，会阻塞主登录流程，影响用户体验。\n考虑把“写入ZSet、清理过期数据、统计数量、判断坏人”这些步骤用线程池或消息队列异步执行，这样的话主登录流程只做“账号密码校验、生成Token”，用户体验会更好一点。\n小贴士：\n异步会有极短的延迟，但是风控允许这种小误差。\n3. 本地缓存+Redis双层缓存 如果恶意设备黑名单查询很频繁，每次都查Redis还是会有压力。\n考虑用本地缓存（比如Caffeine）+ Redis 的双层缓存。\n这样一来本地缓存存“最近1分钟内的恶意设备黑名单”，登录时先查本地缓存，命中直接返回，没命中再查Redis，查到后写入本地缓存。\n小贴士：\n本地缓存不能设 10 分钟，因为多实例部署时，本地缓存互不同步，会导致风控失效。\n设 1 分钟既能减轻 Redis 压力，又能保证安全可控。\n","date":"2026-03-18T10:41:34Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A3%E9%AB%98%E5%B9%B6%E5%8F%91%E5%9C%BA%E6%99%AF%E4%B8%8B%E6%81%B6%E6%84%8F%E7%99%BB%E5%BD%95%E8%AE%BE%E5%A4%87%E8%AF%86%E5%88%AB%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/","title":"【面试真题拆解】高并发场景下“恶意登录设备识别”系统设计"},{"content":"面试官没有直接问我“@Autowired和@Resource的区别”这种题，而是：\n先让我举几个Spring注解的例子，再问“Spring注解定义为什么都挺短，不会有大量代码”，直接考察你对注解本质和Spring AOP/IOC底层的理解。\n举几个Spring注解的例子 1. IOC依赖注入类注解 注解 作用 区别 @Autowired Spring原生注解，按类型（byType） 自动注入依赖 优先byType，找不到唯一Bean时，会尝试按变量名（byName） 匹配；可以配合 @Qualifier 指定Bean名称；可以用 required=false 允许依赖为空 @Qualifier 配合 @Autowired 使用，强制按名称（byName） 注入依赖 当容器中有多个同类型Bean时，必须用它指定具体用哪个 @Resource 按名称（byName） 优先注入，找不到再按类型（byType） 匹配 JSR-250标准，跨框架兼容性好；可以用 name 属性显式指定Bean名称 2. IOC组件注册类注解 注解 作用 @Component 通用组件注册注解，把任意类标记为Spring容器的Bean @Controller @Component 的子类，专门标记Web控制器层的Bean @Service @Component 的子类，专门标记业务逻辑层的Bean @Configuration @Component 的子类，专门标记Java配置类，替代XML配置文件 @Repository @Component 的子类，专门标记数据访问层的 Bean；还能把数据库的异常（如 SQLException）转换成 Spring 的 DataAccessException（非受检异常） 3. AOP切面类注解 横切关注点就是“和业务逻辑无关，但很多地方都要用的功能”，比如日志、事务、权限校验、性能监控这些。\n注解 作用 @Aspect 把类标记为切面类 @Pointcut 定义切入点表达式，告诉Spring“哪些方法需要被增强” @Before 前置通知：在目标方法执行之前执行增强逻辑 @After 后置通知：在目标方法执行之后（不管有没有异常）执行增强逻辑 @AfterReturning 返回通知：在目标方法正常返回之后执行增强逻辑 @AfterThrowing 异常通知：在目标方法抛出异常之后执行增强逻辑 @Around 环绕通知：最强大的通知，可以完全控制目标方法的执行（比如不执行目标方法、修改参数、修改返回值） Lombok的常用注解 Lombok是一种代码简化工具，通过注解自动生成代码。\n虽然不是Spring原生注解，但Lombok的注解是Java开发中经常会用到的。\n注解 作用 @Slf4j 自动生成 SLF4J日志对象 @Data 自动生成 getter/setter、toString、equals、hashCode、无参构造器 @NoArgsConstructor 自动生成 无参构造器 @AllArgsConstructor 自动生成 全参构造器 Spring 注解定义为什么通常都挺短，不会有大量代码？ 面试官这么问主要是想考察你对注解本质和Spring容器底层的理解。\n先明确一下元数据（Metadata） 的定义，元数据就是“描述数据的数据”，本身不是数据的内容，只是给数据打标签、加属性。\n根本原因在于注解只是元数据标记，本身不包含任何逻辑。\n举个Spring注解的例子，比如 @Transactional：\n1 2 3 4 5 6 7 8 9 10 11 12 13 // @Transactional的核心定义（简化版，去掉了很多元注解和属性） @Target({ElementType.METHOD, ElementType.TYPE}) // 元注解：标记这个注解能用在方法/类上 @Retention(RetentionPolicy.RUNTIME) // 元注解：标记这个注解保留到运行时 @Inherited // 元注解：标记这个注解可以被子类继承 @Documented // 元注解：标记这个注解会被生成到JavaDoc里 public @interface Transactional { // 只有属性，没有任何方法/逻辑！ String value() default \u0026#34;\u0026#34;; // 事务管理器的名称 Propagation propagation() default Propagation.REQUIRED; // 传播行为 Isolation isolation() default Isolation.DEFAULT; // 隔离级别 int timeout() default -1; // 超时时间 boolean readOnly() default false; // 是否只读 } 可以看到@Transactional 的定义里只有属性，没有任何一行业务逻辑或容器逻辑。\n它只是告诉Spring容器：“这个方法/类需要开启事务，传播行为是REQUIRED，隔离级别是DEFAULT\u0026hellip;”，真正开启/提交/回滚事务的逻辑，全在Spring的事务管理器和AOP切面里。\n除此之外，Spring注解的定义比较短，还有一个原因是Spring大量使用了元注解（Meta-Annotation）。\n那什么是元注解呢？\n元注解就是“标记注解的注解”，可以复用已有注解的功能，不用重复写代码。\n比如上面的 @Transactional，它上面有@Target、@Retention、@Inherited、@Documented这4个元注解。\n怎么自定义一个Spring注解？ 简单来说有以下3步：\n定义注解 用 @interface 定义，加上JDK原生元注解（至少需要 @Target 和 @Retention(RUNTIME)）；\n编写后置处理器 实现 BeanPostProcessor 或 BeanFactoryPostProcessor 接口，或者用 AOP 切面（更适合方法级别的拦截，比如权限校验、记录执行时间），处理自定义注解的逻辑；\n注册后置处理器 把自定义后置处理器交给Spring容器管理（比如用 @Component 标记）。\n举个例子@AuthCheck：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Target(ElementType.METHOD) //规定这个标签能贴在哪里,ElementType.METHOD 意思是“只能贴在方法上面”。 @Retention(RetentionPolicy.RUNTIME) //规定这个标签的有效期,RUNTIME的意思是“程序运行的时候，这个标签依然存在” public @interface AuthCheck { /** * @interface 不是接口（interface），而是专门用来定义注解的语法。 * String mustRole()：表示用这个标签时， * 你可以写上一个角色名，比如 @AuthCheck(mustRole = \u0026#34;admin\u0026#34;)。 * default \u0026#34;\u0026#34;：如果你只写了 @AuthCheck * 而没填角色，那默认值就是一个空字符串 \u0026#34;\u0026#34; * （表示不需要特定角色，只要登录就行，具体看你怎么实现业务逻辑）。 * */ String mustRole() default \u0026#34;\u0026#34;; } ","date":"2026-03-18T08:53:48Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A3spring%E4%B8%AD%E7%9A%84%E6%B3%A8%E8%A7%A3/","title":"【面试真题拆解】Spring中的注解"},{"content":"以往面试的时候问Java异常好像背一背题就行，但是这个面试官水平还是很高的，换了一种问法，大概是这么问的：\nJava中写了一个函数，打开了一个文件，读这个文件然后关闭，中间没有写try-catch，保存之后，编辑器自动在这个函数声明后面加了throws exception，你觉得这些exception会有哪些呢？\n如果把这个函数进行修改，不读写文件，只进行简单的数据处理，这时保存之后，发现函数声明的后面没有throws这些异常了，在这种情况下是不是就不会抛出异常了？\n有点懵了，完犊子。。。\n主要有以下几点，掌握了应该问题不大：\n文件操作的异常主要是IOException及其子类，属于受检异常，必须显式处理（要么 try-catch，要么 throws）；\n简单数据处理的异常多是RuntimeException 及其子类（如空指针、数组越界），属于非受检异常，无需强制 throws 或 try-catch；\n受检异常要求“编译时必须处理”，非受检异常是“运行时才可能出现”，编译器不强制要求。\n文件操作会抛出哪些异常？ 复现一下面试题里的场景：\n写一个文件读写函数，没加 try-catch，编辑器自动加了 throws Exception。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 import java.io.*; public class FileReadExample { // 编辑器自动加了 throws Exception，因为里面有受检异常 public void readFile(String filePath) throws Exception { // 1. 打开文件流 FileInputStream fis = new FileInputStream(filePath); // 2. 读取文件内容 byte[] buffer = new byte[1024]; int len = fis.read(buffer); // 3. 关闭文件流 fis.close(); } } 有以下几种文件操作中常见的异常\n异常类型 触发场景 FileNotFoundException 文件路径不存在、文件是目录、没有读权限 IOException 读取文件时IO错误（如磁盘损坏、网络文件系统（NFS）的连接意外中断） EOFException 读取到文件末尾仍尝试读取 小贴士：\nFileNotFoundException 是 IOException 的子类，如果 throws IOException，也能覆盖 FileNotFoundException。\n为什么简单数据处理不需要 throws？ 面试题里的第二个场景：\n把函数改成只做简单数据处理，编辑器就不加 throws 了。\n复现一下这个场景：\n1 2 3 4 5 6 7 8 9 10 11 12 13 public class SimpleDataProcessExample { // 编辑器不会加 throws，因为里面都是非受检异常 public void processData(int[] arr, String str) { // 1. 数组越界：ArrayIndexOutOfBoundsException（非受检） int num = arr[10]; // 2. 空指针：NullPointerException（非受检） int len = str.length(); // 3. 非法参数：IllegalArgumentException（非受检） if (arr.length == 0) { throw new IllegalArgumentException(\u0026#34;数组不能为空\u0026#34;); } } } 这里的主要原因是：\n简单数据处理的异常都是 RuntimeException 的子类，是非受检异常，编辑器不会强制要求 throws 或 try-catch。\n受检异常 vs 非受检异常 对比维度 受检异常（Checked Exception） 非受检异常（Unchecked Exception） 定义 编译时必须处理的异常，不处理编译不通过 运行时才可能出现的异常，编译器不强制要求处理 所属类 Exception 及其子类（除了 RuntimeException 及其子类） RuntimeException 及其子类、Error 及其子类 典型例子 IOException、FileNotFoundException NullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException 处理要求 必须 try-catch 或 throws 可以不处理，也可以处理 设计目的 提醒开发者“这里可能有外部不可控的错误，必须处理” 提醒开发者“这里有代码逻辑错误，需要修复代码” 实际开发中，文件操作的异常怎么处理？ 不要直接 throws Exception，推荐显式抛出具体异常（如 throws IOException）或者 try-catch 处理，记录日志并给出友好提示。\n举个例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public void readFile(String filePath) { FileInputStream fis = null; try { fis = new FileInputStream(filePath); byte[] buffer = new byte[1024]; int len = fis.read(buffer); } catch (FileNotFoundException e) { System.err.println(\u0026#34;文件不存在：\u0026#34; + filePath); e.printStackTrace(); } catch (IOException e) { System.err.println(\u0026#34;文件读取失败\u0026#34;); e.printStackTrace(); } finally { // 必须在 finally 里关闭流，防止内存泄漏 if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } } ","date":"2026-03-17T21:09:31Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A3java%E6%96%87%E4%BB%B6%E6%93%8D%E4%BD%9C%E7%9A%84%E5%BC%82%E5%B8%B8%E7%B1%BB%E5%9E%8B%E4%B8%8E%E5%8F%97%E6%A3%80_%E9%9D%9E%E5%8F%97%E6%A3%80%E5%BC%82%E5%B8%B8/","title":"【面试真题拆解】Java文件操作的异常类型与受检_非受检异常"},{"content":"先明确一些核心组件：\n组件 作用 前端 页面展示、用户交互、存储Token API网关 统一入口、路由转发、Token校验 认证中心 用户登录验证、生成/刷新Token 微服务 处理业务逻辑、获取用户信息 数据库 存储用户账号密码、权限信息 当时面试官跟我说，你可以从前端打开一个页面开始讲。\n那我们就举个例子：\n以用户打开一个电商网站首页➡️跳转登录➡️输入账号密码登录➡️携带Token请求商品列表为例，说一下这个完整的流程。\n1.前端打开页面，检查本地是否有Token 用户在浏览器输入 https://www.example.com，前端代码首先检查本地存储中是否存在有效的Token。\n2.没有Token，前端跳转登录页，用户输入账号密码 如果本地没有Token，前端路由跳转到 /login 登录页，用户输入账号和密码，点击【登录】按钮。\n3.前端发送登录请求到API网关 前端将账号密码封装成POST请求，发送到API网关。\n需要注意的是：前端不直接请求认证中心，所有请求都走网关统一入口。\n4.API网关转发登录请求到认证中心 API网关收到 /auth/login 请求，根据路由规则，将请求转发到认证中心服务。\n5.认证中心验证用户信息，生成JWT Token 认证中心做以下3件事：\n从数据库查询用户信息，验证账号密码是否正确； 验证通过后，生成 JWT Token（分为 Access Token 和 Refresh Token）； 将 Token 返回给前端。 那什么是JWT呢？\nJWT（JSON Web Token）是一种无状态的Token，由三部分组成，用 . 分隔：\nHeader：头部，存算法和类型； Payload：载荷，存用户信息（比如userId、username、过期时间）； Signature：签名，用密钥对Header和Payload签名，防止Token被篡改。 6.前端收到Token，存储到本地 前端收到认证中心返回的 access_token 和 refresh_token，存到本地存储中。\n7.前端携带Token请求业务接口 用户登录成功后，前端请求商品列表接口，在请求头 Authorization 中携带 Bearer Token。\nBearer 这个单词的意思是“持票人”、“持有者”。\nBearer Token 就是：“谁持有这个 Token，谁就是合法的访问者，服务器就认谁”。\n8.API网关校验Token API网关收到 /product/list 请求，通过全局过滤器校验Token的有效性：\n从请求头 Authorization 中取出Token； 验证Token的签名是否正确； 验证Token是否过期； 校验通过后，将用户信息（比如userId）放入请求头，转发给微服务； 如果校验失败，直接返回401未授权。 9.微服务获取用户信息，处理业务逻辑 微服务（比如商品服务）收到网关转发的请求，从请求头 X-User-Id 中取出userId，处理业务逻辑（比如查询该用户的商品列表）。\n10.Token过期，前端用Refresh Token刷新 Access Token有效期比较短（比如只有2小时），过期后前端请求会收到401；\n此时前端不用让用户重新登录，而是用Refresh Token去认证中心刷新Access Token。\n","date":"2026-03-17T19:38:35Z","permalink":"https://iamxurulin.github.io/p/%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB-%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%9E%B6%E6%9E%84%E4%B8%8B%E7%9A%84%E7%94%A8%E6%88%B7%E8%AE%A4%E8%AF%81/","title":"前后端分离+微服务架构下的用户认证"},{"content":"一句话速通：\nJava 锁机制核心是 synchronized 和 ReentrantLock，两者都是可重入锁；\nJDK 1.6+ 后 synchronized 引入锁升级（无锁➡️偏向锁➡️轻量级锁➡️重量级锁），性能大幅提升；\n简单场景优先用 synchronized，需要高级功能（比如可中断、可超时、公平锁）时用 ReentrantLock。\n为什么需要锁？ 在并发场景下，多个线程同时修改同一个共享变量，会出现线程安全问题。\n举个例子说明一下：\n1 2 3 4 5 6 7 8 9 10 11 public class UnsafeCounter { private int count = 0; public void increment() { count++; // 这行代码不是原子操作！ } public int getCount() { return count; } } 如果1000个线程同时调用 increment()，最终 count 的值很可能小于1000。\n因为 count++ 不是原子操作，它分为读取count、count+1、写回count三步，多线程同时执行会互相覆盖。\n锁的作用就是把这段代码加锁，让同一时间只有一个线程能执行，从而保证操作的原子性。\nsynchronized synchronized 是JVM内置的关键字，不需要手动释放锁。\n用法 锁的对象 示例 锁实例方法 锁当前对象 this public synchronized void increment() 锁静态方法 锁当前类的 Class 对象 public static synchronized void increment() 锁代码块 锁指定的对象 synchronized (lock) { ... } 可以用synchronized给之前的例子上锁：\n1 2 3 4 5 6 7 8 9 10 11 12 public class SafeCounter { private int count = 0; // 锁实例方法：同一时间只有一个线程能执行 public synchronized void increment() { count++; } public int getCount() { return count; } } ReentrantLock ReentrantLock 是 java.util.concurrent（JUC）包下的锁，是基于代码实现的锁，比 synchronized 更灵活，但需要手动释放锁。\n也可以用ReentrantLock给之前的例子上锁：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockCounter { private int count = 0; // 创建 ReentrantLock 实例 private ReentrantLock lock = new ReentrantLock(); public void increment() { lock.lock(); // 加锁 try { count++; } finally { lock.unlock(); // 必须在 finally 里释放锁，防止死锁 } } public int getCount() { return count; } } ReentrantLock 有3个 synchronized 没有的功能：\n功能 说明 示例 可中断 线程在等待锁的过程中，可以被中断，停止等待 lock.lockInterruptibly() 可超时 尝试获取锁，等待一段时间后如果还没拿到，就放弃 lock.tryLock(1, TimeUnit.SECONDS) 公平锁 按线程请求锁的顺序分配锁（先来先得），默认是非公平锁 new ReentrantLock(true) 可重入锁 synchronized 和 ReentrantLock 都是可重入锁。\n那什么是可重入呢？\n可重入的意思就是同一个线程，可以多次获取同一把锁，不会被自己阻塞。\n那又为什么需要可重入呢？\n举个递归调用的例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class ReentrantExample { // synchronized 是可重入的 public synchronized void methodA() { System.out.println(\u0026#34;执行 methodA\u0026#34;); methodB(); // 调用 methodB，methodB 也需要同一把锁 } public synchronized void methodB() { System.out.println(\u0026#34;执行 methodB\u0026#34;); } public static void main(String[] args) { ReentrantExample example = new ReentrantExample(); example.methodA(); } } 如果锁是不可重入的，那么线程在执行 methodA() 时已经拿到了锁，调用 methodB() 时会因为拿不到锁而被自己阻塞，导致死锁。\nsynchronized 的锁升级 在 JDK 1.6 之前，synchronized 是重量级锁，性能很差；\nJDK 1.6 之后，引入了锁升级机制，提升了 synchronized 的性能。\n锁升级的意思是：\n随着多线程竞争的加剧，锁会从【无锁】➡️【偏向锁】➡️【轻量级锁】➡️【重量级锁】逐步升级，而且升级是单向的，只能升不能降。\nJava对象在内存中分为3部分：\n对象头、实例数据、对齐填充，其中对象头里的 Mark Word 会记录锁的状态。\n而synchronized 的锁是存放在【对象头】里的。\nMark Word 的结构（32 位 JVM）：\n锁状态 25位 4位 1位（是否偏向锁） 2位（锁标志位） 无锁 对象的哈希码 分代年龄 0 01 偏向锁 偏向线程ID 分代年龄 1 01 轻量级锁 指向栈中锁记录的指针 00 重量级锁 指向重量级锁（monitor）的指针 10 锁升级的四个阶段 1. 无锁 没有线程竞争锁，对象处于无锁状态。\n2. 偏向锁 只有一个线程多次获取同一把锁，没有其他线程竞争。\n第一个线程获取锁时，会在对象头的 Mark Word 里记录【偏向线程ID】，这个线程以后再次获取锁时，只需要检查偏向线程ID是不是自己，如果是，直接获取锁。\n当有第二个线程来竞争这把锁时，偏向锁会撤销，升级为轻量级锁。\n3. 轻量级锁 多个线程交替获取锁，但是竞争不太激烈（比如线程A获取锁，执行完释放了，线程B再获取）。\n线程在自己的栈帧里创建一个锁记录（Lock Record），用 CAS（Compare And Swap，比较并交换）操作，尝试把对象头的 Mark Word 替换成指向自己栈中锁记录的指针。\n如果 CAS 成功，就获取到了轻量级锁。\n当有多个线程同时竞争锁，CAS 失败多次，就会升级为重量级锁。\n4. 重量级锁 多个线程同时竞争锁，竞争激烈。\n锁升级为重量级锁后，对象头的 Mark Word 会指向一个monitor（监视器）对象。\n没有获取到锁的线程会进入阻塞队列，被操作系统挂起，等待持有锁的线程释放锁后唤醒。\n","date":"2026-03-17T13:23:22Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A3java%E9%94%81%E6%9C%BA%E5%88%B6synchronizedreentrantlock%E9%94%81%E5%8D%87%E7%BA%A7%E5%8F%AF%E9%87%8D%E5%85%A5%E9%94%81/","title":"【面试真题拆解】Java锁机制：synchronized、ReentrantLock、锁升级、可重入锁"},{"content":"一句话速通：\nJava 锁机制核心是 synchronized 和 ReentrantLock，两者都是可重入锁；\nJDK 1.6+ 后 synchronized 引入锁升级（无锁➡️偏向锁➡️轻量级锁➡️重量级锁），性能大幅提升；\n简单场景优先用 synchronized，需要高级功能（比如可中断、可超时、公平锁）时用 ReentrantLock。\n为什么需要锁？ 在并发场景下，多个线程同时修改同一个共享变量，会出现线程安全问题。\n举个例子说明一下：\n1 2 3 4 5 6 7 8 9 10 11 public class UnsafeCounter { private int count = 0; public void increment() { count++; // 这行代码不是原子操作！ } public int getCount() { return count; } } 如果1000个线程同时调用 increment()，最终 count 的值很可能小于1000。\n因为 count++ 不是原子操作，它分为读取count、count+1、写回count三步，多线程同时执行会互相覆盖。\n锁的作用就是把这段代码加锁，让同一时间只有一个线程能执行，从而保证操作的原子性。\nsynchronized synchronized 是JVM内置的关键字，不需要手动释放锁。\n用法 锁的对象 示例 锁实例方法 锁当前对象 this public synchronized void increment() 锁静态方法 锁当前类的 Class 对象 public static synchronized void increment() 锁代码块 锁指定的对象 synchronized (lock) { ... } 可以用synchronized给之前的例子上锁：\n1 2 3 4 5 6 7 8 9 10 11 12 public class SafeCounter { private int count = 0; // 锁实例方法：同一时间只有一个线程能执行 public synchronized void increment() { count++; } public int getCount() { return count; } } ReentrantLock ReentrantLock 是 java.util.concurrent（JUC）包下的锁，是基于代码实现的锁，比 synchronized 更灵活，但需要手动释放锁。\n也可以用ReentrantLock给之前的例子上锁：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockCounter { private int count = 0; // 创建 ReentrantLock 实例 private ReentrantLock lock = new ReentrantLock(); public void increment() { lock.lock(); // 加锁 try { count++; } finally { lock.unlock(); // 必须在 finally 里释放锁，防止死锁 } } public int getCount() { return count; } } ReentrantLock 有3个 synchronized 没有的功能：\n功能 说明 示例 可中断 线程在等待锁的过程中，可以被中断，停止等待 lock.lockInterruptibly() 可超时 尝试获取锁，等待一段时间后如果还没拿到，就放弃 lock.tryLock(1, TimeUnit.SECONDS) 公平锁 按线程请求锁的顺序分配锁（先来先得），默认是非公平锁 new ReentrantLock(true) 可重入锁 synchronized 和 ReentrantLock 都是可重入锁。\n那什么是可重入呢？\n可重入的意思就是同一个线程，可以多次获取同一把锁，不会被自己阻塞。\n那又为什么需要可重入呢？\n举个递归调用的例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class ReentrantExample { // synchronized 是可重入的 public synchronized void methodA() { System.out.println(\u0026#34;执行 methodA\u0026#34;); methodB(); // 调用 methodB，methodB 也需要同一把锁 } public synchronized void methodB() { System.out.println(\u0026#34;执行 methodB\u0026#34;); } public static void main(String[] args) { ReentrantExample example = new ReentrantExample(); example.methodA(); } } 如果锁是不可重入的，那么线程在执行 methodA() 时已经拿到了锁，调用 methodB() 时会因为拿不到锁而被自己阻塞，导致死锁。\nsynchronized 的锁升级 在 JDK 1.6 之前，synchronized 是重量级锁，性能很差；\nJDK 1.6 之后，引入了锁升级机制，提升了 synchronized 的性能。\n锁升级的意思是：\n随着多线程竞争的加剧，锁会从【无锁】➡️【偏向锁】➡️【轻量级锁】➡️【重量级锁】逐步升级，而且升级是单向的，只能升不能降。\nJava对象在内存中分为3部分：\n对象头、实例数据、对齐填充，其中对象头里的 Mark Word 会记录锁的状态。\n而synchronized 的锁是存放在【对象头】里的。\nMark Word 的结构（32 位 JVM）：\n锁状态 25位 4位 1位（是否偏向锁） 2位（锁标志位） 无锁 对象的哈希码 分代年龄 0 01 偏向锁 偏向线程ID 分代年龄 1 01 轻量级锁 指向栈中锁记录的指针 00 重量级锁 指向重量级锁（monitor）的指针 10 锁升级的四个阶段 1. 无锁 没有线程竞争锁，对象处于无锁状态。\n2. 偏向锁 只有一个线程多次获取同一把锁，没有其他线程竞争。\n第一个线程获取锁时，会在对象头的 Mark Word 里记录【偏向线程ID】，这个线程以后再次获取锁时，只需要检查偏向线程ID是不是自己，如果是，直接获取锁。\n当有第二个线程来竞争这把锁时，偏向锁会撤销，升级为轻量级锁。\n3. 轻量级锁 多个线程交替获取锁，但是竞争不太激烈（比如线程A获取锁，执行完释放了，线程B再获取）。\n线程在自己的栈帧里创建一个锁记录（Lock Record），用 CAS（Compare And Swap，比较并交换）操作，尝试把对象头的 Mark Word 替换成指向自己栈中锁记录的指针。\n如果 CAS 成功，就获取到了轻量级锁。\n当有多个线程同时竞争锁，CAS 失败多次，就会升级为重量级锁。\n4. 重量级锁 多个线程同时竞争锁，竞争激烈。\n锁升级为重量级锁后，对象头的 Mark Word 会指向一个monitor（监视器）对象。\n没有获取到锁的线程会进入阻塞队列，被操作系统挂起，等待持有锁的线程释放锁后唤醒。\n","date":"2026-03-17T13:23:22Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A3java%E9%94%81%E6%9C%BA%E5%88%B6synchronizedreentrantlock%E9%94%81%E5%8D%87%E7%BA%A7%E5%8F%AF%E9%87%8D%E5%85%A5%E9%94%81/","title":"【面试真题拆解】Java锁机制：synchronized、ReentrantLock、锁升级、可重入锁"},{"content":"Redis 内存淘汰策略共 8 种，分两大类：不淘汰和淘汰数据；\n纯缓存场景优先选 allkeys-lru，混合场景选 volatile-lru，数据不能丢的话选 noeviction。\nmaxmemory 参数 只有设置了 maxmemory（Redis 最大内存限制），内存淘汰策略才会生效。\n设置 maxmemory的两种方式 配置文件 redis.conf： 1 2 # 限制 Redis 最大使用 1GB 内存 maxmemory 1gb 命令行动态设置（重启后会失效）： 1 127.0.0.1:6379\u0026gt; CONFIG SET maxmemory 1gb Redis 的8种内存淘汰策略 可以分为两大类：\n大类 策略名称 含义 不淘汰数据 noeviction 内存满了，新写入直接报错，不淘汰任何数据 淘汰过期键 volatile-lru 淘汰设置了过期时间的键中，最近最少使用的 volatile-lfu 淘汰设置了过期时间的键中，最不经常使用的 volatile-ttl 淘汰设置了过期时间的键中，即将过期的 volatile-random 随机淘汰设置了过期时间的键 淘汰所有键 allkeys-lru 淘汰所有键中，最近最少使用的 allkeys-lfu 淘汰所有键中，最不经常使用的 allkeys-random 随机淘汰所有键 1. noeviction 当内存满了，新写入的请求（比如 SET、LPUSH）会直接报错 (error) OOM command not allowed when used memory \u0026gt; 'maxmemory'，不淘汰任何数据。\n适用于数据绝对不能丢，且有完善的内存监控和扩容机制（比如金融级核心数据）的场景。\n2. 淘汰过期键的4种策略 这类策略只淘汰设置了 expire 过期时间的键，不会碰永久键，适合“缓存+持久化”混合的场景（比如永久键存重要数据，过期键存缓存）。\nvolatile-lru LRU = Least Recently Used（最近最少使用）。\n在设置了过期时间的键中，淘汰最久没被访问过的键。\n比如缓存场景，往往会希望保留“最近刚用过的热数据”。\nvolatile-lfu LFU = Least Frequently Used（最不经常使用）。\n在设置了过期时间的键中，淘汰访问次数最少的键。\nvolatile-ttl TTL = Time To Live（剩余生存时间）。\n在设置了过期时间的键中，淘汰剩余时间最短、即将过期的键。\nvolatile-random 在设置了过期时间的键中，随机淘汰一个。\n3. 淘汰所有键的3种策略 这类策略不管键有没有设置过期时间，都会淘汰，适合纯缓存场景（所有数据都是缓存，丢了也没关系，可以从数据库重新加载）。\nallkeys-lru 在所有键中，淘汰最近最少使用的键。\n比较适合电商商品缓存、用户Session缓存这些纯缓存场景。\nallkeys-lfu 在所有键中，淘汰访问次数最少的键。\nallkeys-random 在所有键中，随机淘汰一个。\nLRU vs LFU，到底有啥区别？ 对比维度 LRU（最近最少使用） LFU（最不经常使用） 核心依据 看最近多久没被访问 看访问次数多少 优点 实现简单，符合“热数据最近常用”的直觉 更精准，能识别“真正的热数据”（比如某个键虽然最近没被访问，但总访问次数极高） 缺点 可能误删“偶尔访问一次的冷数据，但其实总访问次数很高” 实现复杂，需要额外空间记录访问次数 举个例子 假设键A昨天被访问100次，今天没被访问；\n键B今天被访问1次。\n因为最近没被访问，所以LRU会删键A。\n但是，因为总访问次数只有1次，所以LFU会删键B。\n","date":"2026-03-16T22:54:44Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A3redis%E7%9A%848%E7%A7%8D%E5%86%85%E5%AD%98%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5/","title":"【面试真题拆解】Redis的8种内存淘汰策略"},{"content":"题目意思很好理解：\n给定一个区间集合，每个区间由 [start, end] 组成，要求合并所有重叠的区间，返回一个不重叠的区间数组，且这个数组要恰好覆盖输入中的所有区间。\n思路 首先，先按每个区间的起始值从小到大排序，这样重叠的区间就会挨在一起，方便后续合并；\n然后初始化一个结果列表，把排序后的第一个区间放进去，作为初始的待合并区间；\n接着遍历剩下的所有区间，每次取出结果列表的最后一个区间和当前区间比较：\n如果当前区间的起始值小于等于最后一个区间的结束值，说明两个区间重叠，就把最后一个区间的结束值更新为两者的最大值，完成合并；\n如果不重叠，就直接把当前区间加入结果列表；\n最后把结果列表转换成二维数组返回即可。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public int[][] merge(int[][] intervals) { // 区间个数为0或1，直接返回 if (intervals.length \u0026lt;= 1) { return intervals; } // 1. 按区间的start值从小到大排序 // a[0]-b[0]表示按start升序 Arrays.sort(intervals, (a, b) -\u0026gt; a[0] - b[0]); // 2. 初始化结果列表，存储合并后的区间 List\u0026lt;int[]\u0026gt; res = new ArrayList\u0026lt;\u0026gt;(); // 先将排序后的第一个区间加入结果列表，作为初始合并区间 res.add(intervals[0]); // 3. 遍历剩下的所有区间，逐个合并 for (int i = 1; i \u0026lt; intervals.length; i++) { // 取出结果列表中最后一个区间 int[] last = res.get(res.size() - 1); // 取出当前遍历的区间 int[] curr = intervals[i]; // 判断是否重叠：当前区间的start \u0026lt;= 最后一个区间的end if (curr[0] \u0026lt;= last[1]) { // 重叠：合并区间，更新最后一个区间的end为两者最大值 last[1] = Math.max(last[1], curr[1]); } else { // 不重叠：直接将当前区间加入结果列表 res.add(curr); } } // 4. 将List\u0026lt;int[]\u0026gt;转换为int[][] return res.toArray(new int[res.size()][]); } ","date":"2026-03-16T17:02:46Z","permalink":"https://iamxurulin.github.io/p/%E6%89%8B%E6%92%95%E7%9C%9F%E9%A2%98%E5%90%88%E5%B9%B6%E5%8C%BA%E9%97%B4/","title":"【手撕真题】合并区间"},{"content":"InnoDB读写磁盘的最小单位是页（Page），默认大小16KB。\n而MySQL的主键索引B+树，每一个树节点（不管是根节点、中间节点、叶子节点），本质上就是一个16KB的页。\n所有计算，都是围绕【1个16KB的页，能存多少东西】展开的。\n节点类型 存的内容 特点 非叶子节点（根节点、中间层节点） 只存主键值 + 指向下一层页的指针 不存完整用户数据，体积极小，一个页能存上千个 叶子节点（最底层节点） 存用户的完整一行行数据 存真实业务数据，体积大，一个页能存的行数有限 1.先算1个非叶子节点，能存多少个索引项 非叶子节点存的是「主键值 + 指针」，我们先算单个索引项的大小：\n主键大小：假设主键是bigint类型，MySQL里bigint固定占8字节； 指针大小：InnoDB里指向页的指针，固定占6字节； 单个索引项总大小：8字节（主键） + 6字节（指针） = 14字节。 一个页是16KB，换算成字节是 16 × 1024 = 16384字节。\n所以一个非叶子页能存的索引项数量：\n16384 ÷ 14 ≈ 1170个\n2.再算1个叶子节点，能存多少行数据 叶子节点存的是用户的完整一行行数据，假设一行业务数据占1KB（比如一张表有id、name、age、create_time等字段，加起来差不多1KB）。\n一个页是16KB，所以一个叶子页能存的行数：\n16KB ÷ 1KB/行 = 16行\n3.最后算三层B+树的总数据量 顺着三层结构，从根节点往下算：\n第一层（根节点）：1个非叶子页，能存1170个索引项 → 能指向1170个第二层的页； 第二层（中间非叶子节点）：1170个非叶子页，每个页又能存1170个索引项 → 能指向 1170 × 1170 = 1368900 个第三层的叶子页； 第三层（叶子节点）：每个叶子页能存16行数据。 最终总数据量 = 叶子页总数 × 每个叶子页的行数：\n1170 × 1170 × 16 ≈ 2190万条\n所以，三层B+树大约能存2000万条记录。\n","date":"2026-03-16T13:09:19Z","permalink":"https://iamxurulin.github.io/p/mysql%E7%9A%84%E4%B8%89%E5%B1%82b-%E6%A0%91%E8%83%BD%E5%AD%98%E5%A4%9A%E5%B0%91%E6%95%B0%E6%8D%AE/","title":"MySQL的三层B+树能存多少数据？"},{"content":"Java IO流的分类 Java IO流主要从两个维度分类：\n分类维度 分类结果 说明 按数据流向分 输入流（Input） 从磁盘/网络/内存「读」数据到程序 输出流（Output） 从程序「写」数据到磁盘/网络/内存 按处理单元分 字节流（Byte Stream） 以「字节（byte）」为单位处理数据，是万能流 字符流（Character Stream） 以「字符（char）」为单位处理数据，专门处理文本 字节流 什么是字节流？ 计算机里所有数据（图片、视频、音频、文本、PDF）本质上都是字节（0和1的组合），所以字节流是万能流，可以处理任何类型的数据。\n字节流的核心类 字节流的抽象基类是 InputStream（输入流）和 OutputStream（输出流），开发中最常用的是它们的子类：\n类名 作用 FileInputStream 从文件中读取字节 FileOutputStream 向文件中写入字节 BufferedInputStream 带缓冲的字节输入流，提升读性能 BufferedOutputStream 带缓冲的字节输出流，提升写性能 举个例子 字节流典型的应用——复制非文本文件（图片、视频、PDF）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class ByteStreamExample { public static void main(String[] args) { // 源文件路径（一张图片） String srcPath = \u0026#34;C:/test/photo.jpg\u0026#34;; // 目标文件路径 String destPath = \u0026#34;C:/test/photo_copy.jpg\u0026#34;; // 用try-with-resources自动关闭流，避免内存泄漏 try ( // 1. 创建文件字节输入流：从源文件读字节 FileInputStream fis = new FileInputStream(srcPath); // 2. 创建文件字节输出流：向目标文件写字节 FileOutputStream fos = new FileOutputStream(destPath) ) { // 3. 定义一个字节数组作为缓冲区（一次读1024字节，即1KB，提升性能） byte[] buffer = new byte[1024]; int len; // 记录每次实际读到的字节数 // 4. 循环读写：fis.read(buffer)返回-1表示读到文件末尾 while ((len = fis.read(buffer)) != -1) { // 只写实际读到的len个字节，避免最后一次写多余的空字节 fos.write(buffer, 0, len); } System.out.println(\u0026#34;图片复制成功！\u0026#34;); } catch (IOException e) { e.printStackTrace(); } } } 字符流 什么是字符流？ 字符流是专门处理文本数据的流，它以【字符】为单位处理数据，内部会自动处理编码转换（比如把UTF-8编码的字节转成Java的char字符）。\n为什么不用字节流处理文本？ 因为中文等非ASCII字符占多个字节（比如UTF-8编码的中文占3个字节）。\n如果用字节流读，可能会把一个汉字的3个字节截断，从而导致乱码；\n而字符流会自动识别编码，按字符读取，不会乱码。\n字符流的核心类 字符流的抽象基类是 Reader（输入流）和 Writer（输出流），开发中最常用的是它们的子类：\n类名 作用 FileReader 从文件中读取字符 FileWriter 向文件中写入字符 BufferedReader 带缓冲的字符输入流 BufferedWriter 带缓冲的字符输出流 InputStreamReader 转换流：字节流转字符流 OutputStreamWriter 转换流：字符流转字节流 举个例子 BufferedReader的readLine()方法可以按行读取文本，非常方便。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; public class CharacterStreamExample { public static void main(String[] args) { String filePath = \u0026#34;C:/test/中文文章.txt\u0026#34;; // 用转换流InputStreamReader，显式指定UTF-8编码，避免乱码 try ( // 1. 先创建字节输入流 FileInputStream fis = new FileInputStream(filePath); // 2. 用转换流把字节流转成字符流，显式指定UTF-8编码 InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8); // 3. 用缓冲流包装，提升性能，且有readLine()方法 BufferedReader br = new BufferedReader(isr) ) { String line; int lineNum = 1; // readLine()返回null表示读到文件末尾 while ((line = br.readLine()) != null) { System.out.println(\u0026#34;第\u0026#34; + lineNum + \u0026#34;行：\u0026#34; + line); lineNum++; } } catch (IOException e) { e.printStackTrace(); } } } ","date":"2026-03-16T10:00:59Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A3java%E5%AD%97%E8%8A%82%E6%B5%81%E5%AD%97%E7%AC%A6%E6%B5%81/","title":"【面试真题拆解】Java字节流、字符流"},{"content":"什么是类加载？ 简单来说，类加载就是把.class文件（编译后的字节码）从磁盘/网络加载到JVM内存里，并生成对应的Class对象的过程。\nJava类加载的5个核心阶段 类加载不是一步到位的，而是分成了5个阶段： 加载➡️验证➡️准备➡️解析➡️初始化， 其中加载、验证、准备、初始化的顺序是固定的，解析可能在初始化之后（为了支持动态绑定）。\n1. 加载（Loading） 这是类加载的第一步，举个例子：假如你要加载User类，JVM会先找到User.class文件，把它的信息存到方法区，然后在堆里生成一个Class对象，以后你要访问User类的信息，都通过这个Class对象。\n2. 验证（Verification） 这一步是为了保证JVM的安全，防止恶意或不规范的.class文件搞破坏，主要验证四个方面：\n文件格式验证：检查.class文件的魔数（开头的CAFEBABE）、版本号（JDK版本兼容）等； 元数据验证：检查类的继承关系（比如不能继承final类）、方法重写是否合法等； 字节码验证：检查字节码指令是否安全（比如不会跳转到非法指令、不会访问越界的数组）； 符号引用验证：检查符号引用（比如类名、方法名）是否能找到对应的类/方法。 3. 准备（Preparation） 这一步是在方法区里为类的静态变量（static修饰的变量）分配内存，并设置初始默认值。\n需要注意的是：不是代码里写的初始值，而是以下这些初始值：\n数据类型 默认值 int 0 long 0L float 0.0f double 0.0d boolean false 引用类型 null 4. 解析（Resolution） 这一步是把常量池里的符号引用（比如类名com.example.User、方法名getUser），转换成直接引用（比如内存地址、指针）。\n解析的话，主要针对的是类、接口、字段、方法等符号引用。\n举个通俗一点的例子：\n符号引用就是一个“代号”，比如你用“张三”指代一个人，不管他在哪，“张三”这个代号都能用；\n而直接引用就是“具体的地址”，比如张三现在在“XX小区XX号楼XX室”，你可以直接找到他。\n5. 初始化（Initialization） 这是类加载的最后一步，也是真正执行程序员写的代码的阶段。\n这一步只做一件事：执行类构造器\u0026lt;clinit\u0026gt;()方法。\n\u0026lt;clinit\u0026gt;()方法是编译器自动生成的，它会把类里的静态变量赋值语句和静态代码块（static {}）按顺序合并起来执行。\n举个例子 1 2 3 4 5 6 7 public class User { static int num = 10; // 静态变量赋值 static { // 静态代码块 num = 20; System.out.println(\u0026#34;静态代码块执行\u0026#34;); } } 以上代码在初始化阶段，JVM会执行\u0026lt;clinit\u0026gt;()方法：\n先把num设为10，再设为20，然后打印“静态代码块执行”。\n什么时候会触发类的初始化？ 有以下6种情况：\n创建类的实例（new User()）； 访问类的静态变量或静态方法（User.num、User.getUser()）； 用Class.forName(\u0026quot;com.example.User\u0026quot;)反射加载类； 初始化子类时，会先初始化父类； JVM启动时，会先初始化包含main()方法的主类； JDK 7+的动态语言支持，MethodHandle解析结果为REF_getStatic/REF_putStatic/REF_invokeStatic的句柄，且对应的类未初始化。 双亲委派机制（Parent Delegation Model） 什么是类加载器？ 类加载器（ClassLoader）就是负责加载类的工具，JVM内置了三种类加载器：\n启动类加载器（Bootstrap ClassLoader） 用C++写的，不是ClassLoader的子类，开发者无法直接获取它的引用。\n负责加载JDK核心类库（比如java.lang.Object、java.lang.String），这些类在jre/lib/rt.jar里。\n扩展类加载器（Extension ClassLoader） 是ClassLoader的子类，开发者可以获取它的引用，负责加载JDK扩展类库（比如jre/lib/ext目录下的类）。\n应用程序类加载器（Application ClassLoader） 是ClassLoader的子类，也是默认的类加载器。负责加载用户自己写的类（比如com.example.User），也就是classpath下的类。\n什么是双亲委派机制？ 这里的“双亲”不是指“父母”，而是指“父类加载器”，是翻译的问题，理解成“父委派机制”更准确一点。\n双亲委派机制的核心规则是：“向上委托，向下加载”\n当一个类加载器收到加载类的请求时，它不会自己先加载，而是把请求委托给父类加载器去加载； 如果父类加载器能加载这个类，就由父类加载器加载； 如果父类加载器加载不了（比如在它的加载范围内找不到这个类），才会向下交给子类加载器自己加载。 举个例子 比如说：加载java.lang.Object\n应用程序类加载器收到加载Object的请求，它不自己加载，委托给扩展类加载器； 扩展类加载器也不自己加载，委托给启动类加载器； 启动类加载器在rt.jar里找到了Object类，直接加载它； 加载完成，返回Class对象，不会再向下交给子类加载器。 为什么需要双亲委派机制？ 双亲委派机制的目的是保证Java程序的安全性和稳定性，主要解决了两个问题：\n避免类的重复加载 如果没有双亲委派，用户自己写一个java.lang.Object类，应用程序类加载器就会加载它，导致JVM里有两个Object类，程序就乱了。\n保证核心类库的安全 防止用户恶意篡改核心类库（比如写一个恶意的java.lang.String类），因为核心类库只会由启动类加载器加载，用户写的同名类不会被加载。\n破坏双亲委派机制的情况 常见的破坏双亲委派机制的场景：\nJDBC的DriverManager JDBC需要加载不同数据库的驱动（比如MySQL驱动），但驱动类是用户写的，启动类加载器加载不了，所以DriverManager用了线程上下文类加载器（Thread Context ClassLoader），打破了双亲委派，让启动类加载器能委托应用程序类加载器加载驱动。\nTomcat的类加载器 Tomcat需要隔离不同Web应用的类（比如两个应用都用了不同版本的Spring），所以它有自己的类加载器结构，每个Web应用有独立的类加载器，优先加载自己的类，而不是向上委托。\n","date":"2026-03-15T22:44:04Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E6%8B%86%E8%A7%A3java%E7%B1%BB%E5%8A%A0%E8%BD%BD%E8%BF%87%E7%A8%8B-%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE%E6%9C%BA%E5%88%B6/","title":"【面试真题拆解】Java类加载过程+双亲委派机制"},{"content":"MySQL的锁按两个维度分，刚好对应开发中最常遇到的问题：\n按解决并发冲突的思路分，是悲观锁和乐观锁——这俩决定了“你是先占坑再干活，还是先干活最后再检查”；\n按锁的范围大小分，是表锁、行锁，还有 InnoDB 特有的间隙锁 + 临键锁 —— 这三类锁决定了 “你是锁整个仓库（表锁）、只锁你要的那一件商品（行锁），还是锁仓库里的某个货架区间（间隙锁 + 临键锁）”。\n先建一张最简单的电商商品表 product，插几条测试数据，等会更好理解举的例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 CREATE TABLE `product` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;主键ID\u0026#39;, `name` varchar(100) NOT NULL COMMENT \u0026#39;商品名称\u0026#39;, `stock` int(11) NOT NULL DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;库存\u0026#39;, `version` int(11) NOT NULL DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;乐观锁版本号\u0026#39;, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=\u0026#39;商品表\u0026#39;; -- 插入测试数据：id=1是iPhone，库存100；id=3是华为，库存200；id=5是小米，库存150 INSERT INTO `product` (`id`, `name`, `stock`, `version`) VALUES (1, \u0026#39;iPhone 17\u0026#39;, 100, 0), (3, \u0026#39;Mate 70\u0026#39;, 200, 0), (5, \u0026#39;Xiaomi 15\u0026#39;, 150, 0); 悲观锁 举个电商秒杀扣库存的真实例子，也是悲观锁最典型的应用：\n比如有两个用户同时秒杀iPhone 17，库存100。\n如果不用锁，俩人同时查出来库存100，都扣减1，就超卖了。\n用悲观锁的话，就是在查库存的时候用 SELECT ... FOR UPDATE 显式加排他锁——\n第一个用户的事务先把这行数据锁住，第二个用户的查询就会阻塞，等第一个用户扣减完库存、提交事务释放锁，第二个用户才能拿到最新的库存，这样就保证了不超卖。\n乐观锁 这也是开发中很常见的场景：两个运营同时修改同一件商品的标题。\n这种场景并发冲突概率很低（因为很少会有两个人同时改同一个商品的标题），如果用悲观锁，每次修改都要加锁，太影响性能了，所以用乐观锁更合适。\n乐观锁的意思就是：我假设没人跟我抢，所以我先直接修改，最后提交的时候再检查一下：\n在我修改的这段时间里，有没有别人也改过这条数据？\n如果没有，就修改成功；\n如果有，就提示用户“数据已被修改，请刷新后重试”。\n举个具体的SQL操作实例 我们打开两个命令行窗口，分表代表两个会话。\n1.会话A（运营A先查商品） 1 2 -- 1. 运营A查出来iPhone的信息，此时 version = 0 SELECT * FROM product WHERE id = 1; 2.会话B（运营B同时查商品） 1 2 -- 1. 运营B也查出来iPhone的信息，此时 version 也是 0 SELECT * FROM product WHERE id = 1; 3.会话A（运营A先提交修改） 1 2 3 4 5 -- 1. 运营A把标题改成“iPhone 17 Pro Max”，WHERE条件带上 version = 0 UPDATE product SET name = \u0026#39;iPhone 17 Pro Max\u0026#39;, version = version + 1 WHERE id = 1 AND version = 0; -- 现象：返回更新行数为 1，修改成功！此时数据库里的 version 变成了 1 4.会话B（运营B后提交修改） 1 2 3 4 5 6 7 -- 1. 运营B把标题改成“iPhone 17 特价版”，WHERE条件也带上 version = 0 UPDATE product SET name = \u0026#39;iPhone 17 特价版\u0026#39;, version = version + 1 WHERE id = 1 AND version = 0; -- 现象：返回更新行数为 0，修改失败！ -- 因为数据库里的 version 已经变成 1 了，找不到 id=1 AND version=0 的行 -- 此时业务代码就可以提示用户：“商品已被其他运营修改，请刷新页面后重试” 表锁 直接锁整张表，其他会话对这张表的任何读写（不管是查哪一行、改哪一行）都会被阻塞。\n举个例子 1 2 3 4 5 6 7 8 9 -- 会话A：给全表商品库存+10，显式加表锁 LOCK TABLES product WRITE; UPDATE product SET stock = stock + 10; -- 此时会话B想查id=1的商品，会直接阻塞！ SELECT * FROM product WHERE id = 1; -- 会话A提交事务并释放表锁后，会话B才能执行 UNLOCK TABLES; 表锁的优点是加锁开销小、速度快，不会死锁，但缺点就是并发度极低，一锁全表，电商场景下用它会直接让网站卡死。\n行锁 只锁具体的某一行数据，其他行完全不受影响，是InnoDB支持高并发的核心。\n需要注意的是行锁的前提是【走索引】。\n如果你的查询没走索引，InnoDB会退化成表锁。\n举两个例子对比一下 先看走索引的情况（行锁生效）：\n1 2 3 4 5 6 7 8 9 10 -- 会话A：用主键id=1查，加排他锁（走主键索引，只锁id=1这行） BEGIN; SELECT * FROM product WHERE id = 1 FOR UPDATE; -- 会话B：查id=3的商品，完全不阻塞！直接拿到锁 BEGIN; SELECT * FROM product WHERE id = 3 FOR UPDATE; -- 执行成功 -- 会话B：查id=1的商品，会阻塞！需要等会话A释放锁 SELECT * FROM product WHERE id = 1 FOR UPDATE; -- 阻塞 再看没走索引的情况（退化成表锁）：\n1 2 3 4 5 6 7 8 -- 先确认name字段没加索引（前面建表的时候只给id加了主键） -- 会话A：用name=\u0026#39;iPhone 17\u0026#39;查，加排他锁（没走索引，锁全表！） BEGIN; SELECT * FROM product WHERE name = \u0026#39;iPhone 17\u0026#39; FOR UPDATE; -- 会话B：查id=3的商品，居然也阻塞了！因为锁了全表 BEGIN; SELECT * FROM product WHERE id = 3 FOR UPDATE; -- 阻塞 行锁的优点是并发度极高，只锁需要的行，不影响其他操作；但缺点是加锁开销大、速度慢，可能产生死锁。\n间隙锁+临键锁 这俩是InnoDB特有的，专门用来解决【幻读】问题的。\n先举个电商场景的例子说明一下什么是幻读？ 1 2 3 4 5 6 7 8 9 10 -- 会话A：查id\u0026gt;1的所有商品（此时表里只有id=1、3、5） BEGIN; SELECT * FROM product WHERE id \u0026gt; 1 FOR UPDATE; -- 第一次查：查到id=3、5两条 -- 会话B：插了一条id=2的商品，提交事务 INSERT INTO product (id, name, stock, version) VALUES (2, \u0026#39;OPPO Find X8\u0026#39;, 100, 0); COMMIT; -- 会话A：再查id\u0026gt;1的商品，居然查到了id=2、3、5三条！ SELECT * FROM product WHERE id \u0026gt; 1 FOR UPDATE; -- 第二次查：多了一条，像产生了幻觉 这就是幻读：同一个事务里，两次相同的范围查询，结果却是不一样的。\n间隙锁（Gap Lock） 间隙锁，间隙锁，顾名思义，它不锁具体的行，锁的是两个索引值之间的“间隙”，也就是只锁两个相邻索引值之间的空白区间，以防止别人在这个区间里插数据。\n以刚开始建的那个product表为例：\nid=1、3、5，间隙就是：(-∞,1)、(1,3)、(3,5)、(5,+∞)。\n临键锁（Next-Key Lock） 临键锁其实是【行锁】+【间隙锁】的组合，锁的是左开右闭的区间。\n比如(1,3]（锁id=3这一行，同时锁1到3之间的间隙）。\n还是以刚开始建的那个product表为例，临键区间就是：(-∞,1]、(1,3]、(3,5]、(5,+∞)。\n举个例子 1 2 3 4 5 6 7 8 9 -- 会话A：用id=3查，加排他锁（走主键索引，加临键锁(1,3]） BEGIN; SELECT * FROM product WHERE id = 3 FOR UPDATE; -- 会话B：想插id=2的商品，直接阻塞！因为间隙(1,3)被锁了 INSERT INTO product (id, name, stock, version) VALUES (2, \u0026#39;OPPO Find X8\u0026#39;, 100, 0); -- 阻塞 -- 会话B：想插id=6的商品，不阻塞！因为(5,+∞)没被锁 INSERT INTO product (id, name, stock, version) VALUES (6, \u0026#39;Vivo X200\u0026#39;, 100, 0); -- 执行成功 小贴士 间隙锁 + 临键锁只在 InnoDB 的默认隔离级别【可重复读（RR）】下生效。\n如果是【读提交（RC）】隔离级别，会关闭间隙锁，只保留行锁。\n夺命连环问：什么是死锁？又怎么避免呢？ 两个或多个会话，互相持有对方需要的锁，都在等待对方释放，导致谁都执行不下去，形成“死循环”，这就是死锁。\n举个例子 1 2 3 4 5 6 7 8 9 10 11 12 13 -- 会话A：先锁id=1，再想锁id=3 BEGIN; SELECT * FROM product WHERE id = 1 FOR UPDATE; -- 持有id=1的锁 -- 此时会话A想拿id=3的锁，但被会话B持有了，阻塞 SELECT * FROM product WHERE id = 3 FOR UPDATE; -- 阻塞 -- 会话B：先锁id=3，再想锁id=1 BEGIN; SELECT * FROM product WHERE id = 3 FOR UPDATE; -- 持有id=3的锁 -- 此时会话B想拿id=1的锁，但被会话A持有了，阻塞 SELECT * FROM product WHERE id = 1 FOR UPDATE; -- 阻塞 -- 结果：死锁！ 死锁的四个必要条件 只要破坏其中任意一个，就能避免死锁：\n互斥条件：一个锁只能被一个会话持有（行锁就是这样，没法破坏）； 请求与保持条件：持有一个锁的同时，又请求另一个锁； 不剥夺条件：锁只能由持有者主动释放，不能被别人强行抢走； 循环等待条件：会话之间形成循环等待链（A等B，B等A）。 怎么避免死锁？ ① 按固定顺序加锁 所有会话都按相同的顺序加锁，比如都先锁id小的，再锁id大的，这样就不会形成循环等待。\n② 减少锁的范围和持有时间 尽量用行锁不用表锁；\n事务尽量小，不要在事务里做无关操作（比如不要在事务里调第三方接口、查无关表），锁持有时间越短，死锁概率越低。\n③ 避免大事务 大事务持有锁的时间长，涉及的行多，更容易死锁，把大事务拆成多个小事务。\n小贴士 InnoDB 有自动死锁检测机制，它会实时扫描锁等待链，一旦发现死锁，会自动回滚其中一个，一般是代价较小的那个事务，比如更新行数少的那个，让另一个事务继续执行，不会让系统无限期卡死。\n","date":"2026-03-15T16:44:46Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E8%83%BD%E8%AE%B2%E8%AE%B2mysql%E7%9A%84%E9%94%81%E6%9C%BA%E5%88%B6%E5%90%97/","title":"【面试真题】能讲讲MySQL的锁机制吗"},{"content":"MTU和MSS，管【单个TCP包到底发多大最合适】，是整个传输的基础；\nRTT和RTO，管【包发出去之后，怎么保证对方一定收到，丢包了怎么办】，是TCP可靠传输的核心；\ncwnd和ssthresh，管【一次能连续发多少个包，怎么既跑满带宽，又不把网络堵死】，是TCP拥塞控制的核心。\nMTU \u0026amp; MSS MTU（最大传输单元，Maximum Transmission Unit） 平时用的以太网、WiFi，硬件层面就规定了一个完整的IP数据包，最大不能超过1500字节，这个上限就是MTU。\n如果IP包超过MTU，就会被拆成好几个小分片传输，只要有一个分片丢了，整个包就得全量重传，特别影响效率，还可能被防火墙拦截。\nMSS（最大报文段长度，Maximum Segment Size） 这是基于MTU算出来的、TCP包里真正能装的业务数据的最大长度——要把IP头、TCP头的空间提前扣掉。\n以太网默认MTU1500字节，标准IP头和TCP头各占20字节，所以默认MSS就是1460字节。\n它是TCP三次握手的时候，客户端和服务端协商出来的，最终取两边的最小值，目的就是从源头避免IP拆包。\nRTT \u0026amp; RTO RTT（往返时延，Round-Trip Time） 我发一个TCP包出去，到我收到对方回的【我收到了】的ACK确认包，这一来一回花的时间，就是RTT。\n它是网络环境决定的客观值，网络越拥堵、跨的路由越多，RTT就越长，会实时波动，是算RTO的唯一基础。\nRTO（重传超时时间，Retransmission TimeOut） 我发出去一个包，会设一个定时器，要是超过这个时间还没收到ACK，我就认为这个包丢了，马上重发一个，这个定时器的时间就是RTO。\nRTO完全是基于RTT算出来的，不是固定死的。\n它必须比RTT大一点。\n如果太小了，ACK还在路上就瞎重传，浪费带宽；\n但太大了，真丢包了半天不重传，传输变慢。\n所以，网络波动RTT变了，RTO也会跟着实时调整。\ncwnd \u0026amp; ssthresh 这两个参数是TCP拥塞控制的核心，TCP刚建连的时候，根本不知道当前网络能扛住多大流量，发少了浪费带宽，发多了直接把网络堵死，所以用这两个参数控制发送节奏。\ncwnd（拥塞窗口，Congestion Window） TCP发送端自己维护的【油门】，单位是MSS。这个参数决定了在没收到对方ACK之前，我最多能连续发多少个包。\n比如cwnd=10，就是最多连续发10个MSS大小的包，不用等ACK。\n需注意的是，cwnd不是固定的，网络越好、丢包越少，cwnd就越大，发送速度就越快；一旦丢包，就会马上降下来。\nssthresh（慢启动阈值，Slow Start Threshold） cwnd的【换挡开关】，用来划分两个发送阶段，单位也是MSS。\n当cwnd ＜ ssthresh时，进入慢启动阶段，cwnd每过一个RTT就直接翻倍，指数级快速涨速，尽快把带宽跑满；\n当cwnd ≥ ssthresh时，进入拥塞避免阶段，cwnd每过一个RTT才加1，线性慢慢涨，避免冲太猛把网络堵死。\n一旦检测到网络拥堵丢包，ssthresh会立刻降到当前cwnd的一半，再根据丢包的严重程度调整cwnd，重新控制发送节奏。\n","date":"2026-03-15T11:36:23Z","permalink":"https://iamxurulin.github.io/p/tcp%E7%9A%84%E6%A0%B8%E5%BF%83%E5%8F%82%E6%95%B0-mtumssrttrtocwndssthresh/","title":"TCP的核心参数-MTU、MSS、RTT、RTO、cwnd、ssthresh"},{"content":"为什么需要垃圾回收？ 首先得说清楚，JVM的垃圾回收（GC）到底是干嘛的。\n其实就像租房子住，时间长了会产生垃圾，要是不打扫，房子就没法住了。\n在Java里，我们写代码会创建很多对象（比如new Person()、new ArrayList()），这些对象都存在堆内存里。\n但是，堆内存是有限的，要是对象用完了不清理，内存就会被占满，最后报OutOfMemoryError（OOM），程序就挂了。\n所以GC的作用就是：自动找到堆里那些“没人用的垃圾对象”，把它们清理掉，腾出内存空间。\n堆内存划分 JVM把堆分成了新生代和老年代两大块：\n新生代：刚创建的对象优先放在这里。就像“幼儿园”，大部分对象“朝生夕死”（比如方法里的临时变量，方法执行完就没用了）。 老年代：在新生代里“熬过好几次GC”的对象，会被放到这里。就像“养老院”，这里的对象大多活得比较久（比如Spring容器里的单例Bean）。 那为什么要这么划分呢？\n主要还是因为不同生命周期的对象，用不同的回收算法，效率会更高一点。\n各代用什么垃圾回收算法？ 我当时面试的时候突然懵了，说的不知道， 裂开。。。\n对新生代来说，它的特点是：垃圾多，存活对象少（比如100个对象里，可能只有2个活着）。\n采用的是复制算法。\nJVM把新生代分成三块：\n1个Eden区（占80%）和2个Survivor区（From和To，各占10%）。\n平时只用Eden区和其中一个Survivor区（比如From）。\n新对象先在Eden区分配，当Eden区满了，触发Minor GC（新生代GC），把Eden和From区里活着的对象，复制到To区，然后直接清空Eden和From区，From和To交换角色（下次用Eden和新的From区）。\n对老年代来说，它的特点是：存活对象多，垃圾少，而且没有额外的大空间做复制担保，所以不用复制算法，采用的是**标记-清除 **或 标记-整理算法。\n标记-清除算法（Mark-Sweep） 这种算法的实现逻辑是，从“GC Roots”（比如主线程、静态变量这些“根”）出发，找到所有活着的对象，做上标记，然后遍历堆内存，把没标记的对象（垃圾）清理掉。\n优点是实现简单，但是会产生内存碎片（比如清理后剩下很多小空间，大对象可能放不下）。\n标记-整理算法（Mark-Compact） 为了解决内存碎片问题，在“标记-清除”基础上加了个“整理”步骤。\n实现逻辑和标记清除算法一样，先标记存活对象，把所有存活对象往内存的一端移动，按顺序排好，最后，直接清空边界以外的内存。\n这种算法的优点是没有内存碎片，但缺点是需要移动对象，效率比“标记-清除”低。\n常见的垃圾回收器 Serial：单线程回收，适合客户端应用（比如桌面软件）； ParNew：Serial的多线程版，常和CMS配合用； CMS（Concurrent Mark Sweep）：主打低延迟（垃圾回收时尽量不让程序停顿太久），适合互联网应用（比如电商后台），用的是“标记-清除”算法； G1（Garbage-First）：把堆分成很多小区域，优先回收垃圾多的区域，平衡了吞吐量和延迟。 ","date":"2026-03-14T19:26:46Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E8%AE%B2%E8%AE%B2jvm%E7%9A%84%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6/","title":"【面试真题】讲讲JVM的垃圾回收机制？"},{"content":"首先，官方的定义是这样的：\nJava反射（Reflection）是指在程序运行时，动态获取类的完整结构信息（如构造方法、字段、方法），并能直接操作对象的属性或方法的机制。\n正射 vs 反射 用【创建对象并调用方法】举个例子：\n正射 就是我们平时写代码的方式，编译期就知道要操作的类是什么，直接 new 对象调用方法：\n1 2 3 // 正射：编译期就确定是User类 User user = new User(); user.setName(\u0026#34;张三\u0026#34;); // 直接调用方法 反射 动态的方式**，编译期**不知道要操作的类是什么，运行期才通过类名字符串动态获取类信息并操作：\n1 2 3 4 5 // 反射：运行期才通过字符串\u0026#34;com.example.User\u0026#34;获取类 Class\u0026lt;?\u0026gt; clazz = Class.forName(\u0026#34;com.example.User\u0026#34;); Object user = clazz.getDeclaredConstructor().newInstance(); // 动态创建对象 Method setNameMethod = clazz.getDeclaredMethod(\u0026#34;setName\u0026#34;, String.class); setNameMethod.invoke(user, \u0026#34;张三\u0026#34;); // 动态调用方法 原理 反射的底层，其实是操作JVM加载类后生成的 Class对象。\n先回顾一下类的加载过程：\n编译期：.java文件被编译成 .class字节码文件； 类加载期：JVM的类加载器（ClassLoader）把 .class文件加载到内存，生成一个 java.lang.Class对象（这个对象包含了类的所有结构信息）； 运行期：反射就是通过这个 Class对象，逆向获取类的构造方法、字段、方法等信息，并进行操作。 说人话就是：Class对象是反射的入口，所有反射操作都从获取Class对象开始。\n手撕一下，加深理解 1. 获取Class对象的3种方式 1 2 3 4 5 6 7 8 9 // 方式1：类名.class（最常用，编译期就能获取） Class\u0026lt;User\u0026gt; clazz1 = User.class; // 方式2：对象.getClass()（已有对象时用） User user = new User(); Class\u0026lt;? extends User\u0026gt; clazz2 = user.getClass(); // 方式3：Class.forName(\u0026#34;全类名\u0026#34;)（最灵活，框架常用） Class\u0026lt;?\u0026gt; clazz3 = Class.forName(\u0026#34;com.example.User\u0026#34;); 2. 操作构造方法：动态创建对象 1 2 3 4 5 6 7 8 9 Class\u0026lt;?\u0026gt; clazz = Class.forName(\u0026#34;com.example.User\u0026#34;); // 获取无参构造方法并创建对象 Constructor\u0026lt;?\u0026gt; noArgConstructor = clazz.getDeclaredConstructor(); Object user1 = noArgConstructor.newInstance(); // 获取有参构造方法并创建对象 Constructor\u0026lt;?\u0026gt; argConstructor = clazz.getDeclaredConstructor(String.class, int.class); Object user2 = argConstructor.newInstance(\u0026#34;张三\u0026#34;, 25); 3. 操作字段：动态获取/设置属性 1 2 3 4 5 6 7 8 9 10 11 Class\u0026lt;?\u0026gt; clazz = Class.forName(\u0026#34;com.example.User\u0026#34;); Object user = clazz.getDeclaredConstructor().newInstance(); // 获取public字段并赋值 Field nameField = clazz.getField(\u0026#34;name\u0026#34;); nameField.set(user, \u0026#34;张三\u0026#34;); // 获取private字段并赋值（需打破封装） Field ageField = clazz.getDeclaredField(\u0026#34;age\u0026#34;); ageField.setAccessible(true); // 关键：打破private访问限制 ageField.set(user, 25); 4. 操作方法：动态调用方法 1 2 3 4 5 6 7 8 9 10 11 Class\u0026lt;?\u0026gt; clazz = Class.forName(\u0026#34;com.example.User\u0026#34;); Object user = clazz.getDeclaredConstructor().newInstance(); // 获取public方法并调用 Method setNameMethod = clazz.getMethod(\u0026#34;setName\u0026#34;, String.class); setNameMethod.invoke(user, \u0026#34;张三\u0026#34;); // 获取private方法并调用（需打破封装） Method getAgeMethod = clazz.getDeclaredMethod(\u0026#34;getAge\u0026#34;); getAgeMethod.setAccessible(true); int age = (int) getAgeMethod.invoke(user); 实际应用场景 Java生态里的核心框架几乎都用到了反射，比如：\n1. Spring框架的IOC/DI Spring的IOC容器就是通过反射创建对象并注入依赖的：\n在XML里写 \u0026lt;bean id=\u0026quot;user\u0026quot; class=\u0026quot;com.example.User\u0026quot;/\u0026gt;➡️Spring读取XML，拿到全类名 com.example.User➡️Spring通过 Class.forName() 获取Class对象，动态创建Bean对象➡️如果Bean有依赖（比如 userService 依赖 userDao），Spring再通过反射注入依赖。\n2. JDBC加载数据库驱动 Class.forName(\u0026quot;com.mysql.cj.jdbc.Driver\u0026quot;)，底层就是反射：首先加载驱动类到JVM，接下来驱动类的静态代码块会自动执行，把驱动注册到 DriverManager 里。\n3. 注解处理 比如JUnit的 @Test 注解：JUnit启动后，通过反射扫描所有类，找到带 @Test 注解的方法，再通过反射动态调用这些方法执行测试。\n反射的优缺点 优点 灵活性高：运行期才确定要操作的类，不需要在编译期写死； 动态性强：适合开发通用框架（如Spring、MyBatis），框架不需要知道用户会写什么类，通过反射就能动态适配。 缺点 性能低：反射比直接调用方法慢10-100倍（因为需要动态解析类信息），所以高频调用的场景（比如循环里）尽量不用反射； 破坏封装性：可以通过 setAccessible(true) 访问private字段/方法，破坏了Java的封装原则； 安全性问题：反射可以执行任意代码，可能被恶意利用。 ","date":"2026-03-14T12:16:58Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98java%E4%B8%AD%E4%BB%80%E4%B9%88%E6%98%AF%E5%8F%8D%E5%B0%84/","title":"【面试真题】Java中什么是反射"},{"content":"坦白说，这个问题本质上不是个纯技术方案题，而是高并发场景下，平衡【性能、可用性、一致性强度】的工程题。\n核心是先看业务的一致性容忍度，再选对应方案，而且必须做好异常兜底，不然线上一定会出永久脏数据。\n在电商场景中，绝大多数情况下优先保证最终一致性，只有金融级核心链路才会考虑强一致性。\n先说标准流程：\n读请求：先查缓存，命中直接返回；没命中就读数据库，把结果写回缓存再返回。 写请求：先更新数据库，再删除缓存。 为什么是【删除缓存】，而不是【更新缓存】？ 一是并发写场景下，更新缓存一定会有数据覆盖的风险；\n二是电商场景很多缓存值是多表关联算出来的，更新缓存的成本极高，不如删除后懒加载；\n三是写多读少的场景，频繁更新缓存全是无效操作，浪费性能。\n为什么是【先更库，再删缓存】，不能反过来？ 如果你刚把缓存删了，数据库还没更新完，这时候高并发的读请求进来，缓存查不到，直接去数据库拉了旧数据，还写回了缓存。\n等你数据库更新完，缓存里的旧数据就一直躺在那，除非缓存过期，不然用户一直看到的是错的数据。\n但是，如果是先更库再删缓存，只有极小概率会出问题，比如：\n读请求缓存没命中，正在读数据库的瞬间，写请求更新了数据库并删了缓存，之后读请求把旧数据写回缓存。\n但是，但是啊，这个场景发生的条件太苛刻了，因为写数据库的耗时远大于读数据库，基本卡不中这个时间窗口，线上出问题的概率几乎可以忽略。\n就算真的撞上了，还有缓存 TTL 兜底，最多就是短时间不一致，绝对不会出永久脏数据。\n其实，这个方案最大的风险，是第二步删缓存失败。\n同步删缓存如果失败，立刻把这个key丢到消息队列里做异步指数退避重试，设置最大重试次数，绝对不让缓存操作阻塞主流程。\n高并发下的两个进阶方案 第一个是延迟双删，解决基础方案的极端并发问题。\n“针对商品详情、营销活动这种超高并发读的场景，用延迟双删做加固，流程是：\n先删除缓存➡️更新数据库➡️延迟500ms-1s，二次删除缓存。\n第二次延迟删除，刚好能覆盖掉【数据库更新过程中，读请求写回缓存的旧数据】，彻底解决极端并发的脏数据问题。\n其中，延迟时间必须大于【业务读请求的最大耗时 + 数据库主从同步延迟】。\n第二个是基于Binlog的异步缓存淘汰。\n用Binlog异步淘汰缓存，完全解耦业务和缓存操作。\n业务代码只做一件事：更新数据库，完全不碰缓存。\n然后用Canal监听数据库的binlog增量日志，只有主库事务提交成功，才会拿到数据变更事件，再异步去删除对应的缓存key。如果删除失败，就丢进死信队列重试，超过阈值直接触发告警。\n强一致性方案 只有像支付账户余额、核心库存扣减这种，一分钱都不能错、零容错的场景，才会考虑强一致性方案，因为它的性能代价极大。\n用Redisson的分布式读写锁，写请求加写锁，读请求加读锁，读写互斥，强制串行化，彻底避免并发问题。\n但这个方案会严重降低系统吞吐量，高并发下很容易出现锁等待、超时问题，所以非核心场景，绝对不会用。\n兜底机制 不管方案设计得多完美，线上一定会有意外，比如：\n缓存集群炸了、消息队列挂了、binlog消费延迟了，没有兜底，就等着出线上事故。\n可以采用以下兜底策略：\nTTL兜底：所有缓存key必须设置合理的过期时间，哪怕所有机制都失效了，最多就是一段时间的不一致，缓存一过期，就自动拉取最新数据，绝对不会出现永久脏数据。 失败重试机制：所有缓存删除操作，必须有异步重试兜底，不能一次失败就放任脏数据存在。 降级开关：大促场景下，如果缓存集群扛不住了，直接开降级开关，所有请求切到读数据库，先保证用户能下单，别的都好说。 ","date":"2026-03-13T14:14:10Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E7%9C%9F%E9%A2%98%E7%BC%93%E5%AD%98%E5%92%8C%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E4%B8%80%E8%87%B4%E6%80%A7%E9%97%AE%E9%A2%98%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81/","title":"【面试真题】缓存和数据库的一致性问题如何保证？"},{"content":"思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 public class TreeDistance { /** * 计算两个节点在二叉树中的距离 * * @param root 二叉树的根节点 * @param p 第一个节点 * @param q 第二个节点 * @return 两个节点之间的距离 */ public int distanceBetweenNodes(TreeNode root, TreeNode p, TreeNode q) { // 首先找到两个节点的最近公共祖先(LCA) TreeNode lca = findLCA(root, p, q); // 计算节点p到LCA的深度 int depthP = findDepth(lca, p); // 计算节点q到LCA的深度 int depthQ = findDepth(lca, q); // 两个节点之间的距离等于它们到LCA的深度之和 return depthP + depthQ; } /** * 查找二叉树中两个节点的最近公共祖先(LCA) * * @param root 二叉树的根节点 * @param p 第一个目标节点 * @param q 第二个目标节点 * @return 最近公共祖先节点，如果其中一个节点是根节点，则返回该节点；如果两个节点分别在左右子树中，则返回它们的根节点 */ private TreeNode findLCA(TreeNode root, TreeNode p, TreeNode q) { // 如果根节点为空，或者根节点是p或q中的一个，直接返回根节点 if (root == null || root == p || root == q) { return root; } // 递归查找左子树中的LCA TreeNode left = findLCA(root.left, p, q); // 递归查找右子树中的LCA TreeNode right = findLCA(root.right, p, q); // 如果左右子树都找到了非空节点，说明p和q分别在左右子树中，当前节点就是LCA if (left != null \u0026amp;\u0026amp; right != null) { return root; } // 如果左子树找到非空节点，则返回左子树的结果，否则返回右子树的结果 return left != null ? left : right; } /** * 查找目标节点在给定祖先节点下的深度 * * @param ancestor 起始祖先节点 * @param target 目标节点 * @return 目标节点相对于祖先节点的深度，如果未找到则返回-1 */ private int findDepth(TreeNode ancestor, TreeNode target) { // 如果祖先节点为空，说明无法找到目标节点，返回-1 if (ancestor == null) { return -1; } // 如果当前节点就是目标节点，深度为0 if (ancestor == target) { return 0; } // 递归查找左子树 int left = findDepth(ancestor.left, target); // 如果在左子树中找到目标节点，返回左子树深度+1 if (left != -1) { return left + 1; } // 递归查找右子树 int right = findDepth(ancestor.right, target); // 如果在右子树中找到目标节点，返回右子树深度+1，否则返回-1 return right != -1 ? right + 1 : -1; } } ","date":"2026-03-11T22:26:31Z","permalink":"https://iamxurulin.github.io/p/%E6%89%8B%E6%92%95%E7%9C%9F%E9%A2%98-%E8%AE%A1%E7%AE%97%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E4%B8%A4%E4%B8%AA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E8%B7%9D%E7%A6%BB/","title":"手撕真题-计算二叉树中两个节点之间的距离"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 // 存储并查集中节点 i 的父节点 private static int[] parent; // 存储第 i 朵云（或合并后以 i 为根的商品组）的总价钱 private static int[] cost; // 存储第 i 朵云（或合并后以 i 为根的商品组）的总价值 private static int[] value; public static void main(String[] args) throws IOException { // 使用 BufferedReader 和 PrintWriter 提升大规模数据下的 I/O 效率 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 1. 读取基础配置：n 朵云, m 个搭配关系, w 现有的钱 String[] strA = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int n = Integer.parseInt(strA[0]); int m = Integer.parseInt(strA[1]); int w = Integer.parseInt(strA[2]); parent = new int[n + 1]; cost = new int[n + 1]; value = new int[n + 1]; // 2. 初始化每朵云的属性，并初始化并查集（每个节点初始父节点为自己） for (int i = 1; i \u0026lt;= n; i++) { String[] strB = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); cost[i] = Integer.parseInt(strB[0]); value[i] = Integer.parseInt(strB[1]); parent[i] = i; } // 3. 处理搭配购买关系：使用并查集将必须一起买的云“联通”起来 for (int i = 0; i \u0026lt; m; i++) { String[] strC = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int u = Integer.parseInt(strC[0]); int v = Integer.parseInt(strC[1]); union(u, v); } // 4.将同一集合内所有云的价钱和价值全部累加到该集合的“根节点”上 for (int i = 1; i \u0026lt;= n; i++) { int root = find(i); // 如果当前节点不是根节点，则将其属性转移给根节点 if (root != i) { cost[root] += cost[i]; value[root] += value[i]; // 清空当前非根节点的属性，防止在后续背包计算中被重复统计 cost[i] = 0; value[i] = 0; } } // 5. 执行 0/1 背包算法 // dp[j] 表示在花费 j 元钱的情况下能获得的最大价值 int[] dp = new int[w + 1]; for (int i = 1; i \u0026lt;= n; i++) { // 只处理根节点（代表一个完整的“捆绑商品组”） if (parent[i] == i) { // 0/1 背包内层循环必须“逆序遍历”，防止同一商品被多次购买 for (int j = w; j \u0026gt;= cost[i]; j--) { // 状态转移方程：不买当前组 vs 买当前组（消耗钱，加价值） dp[j] = Math.max(dp[j], dp[j - cost[i]] + value[i]); } } } // 6. 输出预算 w 内能获得的最大总价值 out.println(dp[w]); // 刷新缓冲区并释放资源 out.flush(); out.close(); br.close(); } /** * 查找根节点（带路径压缩优化） */ private static int find(int i) { if (parent[i] == i) { return i; } // 将查找路径上的所有节点直接挂在根节点下，加速后续查找 return parent[i] = find(parent[i]); } /** * 合并两个集合 */ private static void union(int i, int j) { int rootI = find(i); int rootJ = find(j); if (rootI != rootJ) { // 将 I 树合并到 J 树下 parent[rootI] = rootJ; } } ","date":"2026-03-10T20:41:45Z","permalink":"https://iamxurulin.github.io/p/bishi103-%E6%A8%A1%E6%9D%BF%E6%9C%89%E4%BE%9D%E8%B5%96%E7%9A%84%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98/","title":"BISHI103 【模板】有依赖的背包问题"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 // 存储节点 i 的父节点编号；当 parent[i] == i 时，i 为该集合的根节点 private static int[] parent; // 仅在 i 为根节点时有效，记录以 i 为根的集合中包含的元素总数 private static int[] size; public static void main(String[] args) throws IOException { // 使用高性能输入输出流 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 解析输入首行的 n (元素总数) 和 q (操作总数) String[] strA = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int n = Integer.parseInt(strA[0]); int q = Integer.parseInt(strA[1]); parent = new int[n + 1]; size = new int[n + 1]; for (int i = 1; i \u0026lt;= n; i++) { // 初始状态下，每个元素自成一个集合，父节点指向自己 parent[i] = i; // 每个初始集合的大小均为 1 size[i] = 1; } while (q-- \u0026gt; 0) { String[] strB = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int op = Integer.parseInt(strB[0]); // 操作类型编号 switch (op) { case 1: { // 合并操作：将 i 和 j 所在的两个集合合并为一个 int i = Integer.parseInt(strB[1]); int j = Integer.parseInt(strB[2]); union(i, j); break; } case 2: { // 判断 i 和 j 是否属于同一个集合 int i = Integer.parseInt(strB[1]); int j = Integer.parseInt(strB[2]); // 若两者的根节点相同，则在同一集合，输出 YES out.println(find(i) == find(j) ? \u0026#34;YES\u0026#34; : \u0026#34;NO\u0026#34;); break; } case 3: { // 获取 i 所在集合的元素总数 int i = Integer.parseInt(strB[1]); // 必须先通过 find(i) 找到根节点，因为只有根节点的 size 是准确的 out.println(size[find(i)]); break; } } } // 刷新缓冲区并释放 IO 资源 out.flush(); out.close(); br.close(); } /** * 查找元素 i 所在集合的根节点 * 在递归查找过程中，将路径上所有节点的父节点直接指向根节点 */ private static int find(int i) { // 如果自己就是根节点，直接返回 if (parent[i] == i) { return i; } // 将查找到的根节点赋值给当前节点的 parent 数组 return parent[i] = find(parent[i]); } /** * 将包含 i 和 j 的两个集合合并 * * @param i 第一个元素 * @param j 第二个元素 */ private static void union(int i, int j) { // 分别获取两个元素当前的根节点 int rootI = find(i); int rootJ = find(j); // 如果根节点不同，说明处于不同集合，执行合并 if (rootI != rootJ) { // 将 I 集合的根节点挂载到 J 集合的根节点之下 parent[rootI] = rootJ; // 将被合并集合的规模累加到新的根节点 (rootJ) 上 size[rootJ] += size[rootI]; } } ","date":"2026-03-10T20:04:55Z","permalink":"https://iamxurulin.github.io/p/bishi102-%E6%A8%A1%E6%9D%BF%E5%B9%B6%E6%9F%A5%E9%9B%86/","title":"BISHI102 【模板】并查集"},{"content":" 思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，使用PrintWriter输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取测试用例数量T int T = Integer.parseInt(br.readLine().trim()); // 处理每个测试用例 while (T-- \u0026gt; 0) { // 读取树的节点数n int n = Integer.parseInt(br.readLine().trim()); // 创建邻接表表示树 List\u0026lt;Integer\u0026gt;[] adj = new ArrayList[n + 1]; // 初始化邻接表 for (int i = 1; i \u0026lt;= n; i++) { adj[i] = new ArrayList\u0026lt;\u0026gt;(); } // 创建度数数组 int[] deg = new int[n + 1]; // 读取n-1条边，构建邻接表和度数数组 for (int i = 0; i \u0026lt; n - 1; i++) { String[] edge = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int u = Integer.parseInt(edge[0]); int v = Integer.parseInt(edge[1]); // 无向图，双向添加边 adj[u].add(v); adj[v].add(u); // 更新节点的度数 deg[u]++; deg[v]++; } // 创建距离数组，初始化为-1 int[] dist = new int[n + 1]; Arrays.fill(dist, -1); // 创建队列用于BFS Queue\u0026lt;Integer\u0026gt; queue = new LinkedList\u0026lt;\u0026gt;(); // 将所有度为1的节点（叶子节点）加入队列，距离设为0 for (int i = 1; i \u0026lt;= n; i++) { if (deg[i] == 1) { dist[i] = 0; queue.add(i); } } // BFS遍历树，计算每个节点到最近叶子节点的距离 while (!queue.isEmpty()) { int u = queue.poll(); // 遍历当前节点的所有邻接节点 for (int v : adj[u]) { if (dist[v] == -1) { // 更新距离并加入队列 dist[v] = dist[u] + 1; queue.add(v); } } } // 找到最大距离 int maxDist = -1; for (int i = 1; i \u0026lt;= n; i++) { // 只考虑非叶子节点 if (deg[i] \u0026gt; 1) { maxDist = Math.max(maxDist, dist[i]); } } // 收集所有距离为最大距离的非叶子节点（中心点） List\u0026lt;Integer\u0026gt; mikuPoints = new ArrayList\u0026lt;\u0026gt;(); for (int i = 1; i \u0026lt;= n; i++) { if (deg[i] \u0026gt; 1 \u0026amp;\u0026amp; dist[i] == maxDist) { mikuPoints.add(i); } } // 输出中心点的数量 out.println(mikuPoints.size()); // 输出所有中心点 for (int i = 0; i \u0026lt; mikuPoints.size(); i++) { out.print(mikuPoints.get(i) + (i == mikuPoints.size() - 1 ? \u0026#34;\u0026#34; : \u0026#34; \u0026#34;)); } out.println(); } // 刷新输出缓冲区，确保所有输出都被写入 out.flush(); // 关闭输出流 out.close(); // 关闭输入流 br.close(); } ","date":"2026-03-09T22:05:01Z","permalink":"https://iamxurulin.github.io/p/bishi101-%E4%B8%96%E7%95%8C%E6%A0%91%E4%B8%8A%E6%89%BE%E7%B1%B3%E5%BA%93/","title":"BISHI101 世界树上找米库"},{"content":" 思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 public static void main(String[] args) throws IOException { // 使用 BufferedReader 和 PrintWriter 提升大规模数据下的 I/O 效率 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取输入的第一行，包含两个整数n和m，分别表示顶点数和边数 String[] strA = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int n = Integer.parseInt(strA[0]); // 顶点数 int m = Integer.parseInt(strA[1]); // 边数 // 创建邻接表表示的无向图 List\u0026lt;Integer\u0026gt;[] adj = new ArrayList[n + 1]; for (int i = 1; i \u0026lt;= n; i++) { adj[i] = new ArrayList\u0026lt;\u0026gt;(); } // 读取所有边的信息，构建无向图 for (int i = 0; i \u0026lt; m; i++) { String[] edge = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int u = Integer.parseInt(edge[0]); // 边的一个端点 int v = Integer.parseInt(edge[1]); // 边的另一个端点 adj[u].add(v); // 添加边到邻接表 adj[v].add(u); // 无向图，双向添加 } // color数组用于记录每个顶点的颜色，0表示未染色，1和2表示两种不同的颜色 int[] color = new int[n + 1]; boolean flag = true; // 标记是否为二分图 // 使用队列实现BFS Queue\u0026lt;Integer\u0026gt; queue = new LinkedList\u0026lt;\u0026gt;(); // 从顶点1开始染色，并将其加入队列 queue.add(1); color[1] = 1; // BFS遍历图 while (!queue.isEmpty()) { int cur = queue.poll(); // 取出队首顶点 // 遍历当前顶点的所有邻接顶点 for (int neighbor : adj[cur]) { // 如果邻接顶点未被染色 if (color[neighbor] == 0) { // 将邻接顶点染成与当前顶点不同的颜色 color[neighbor] = 3 - color[cur]; queue.add(neighbor); // 将邻接顶点加入队列 } else if (color[neighbor] == color[cur]) { // 如果邻接顶点与当前顶点颜色相同，则不是二分图 flag = false; break; } } } // 输出结果：如果是二分图输出\u0026#34;YES\u0026#34;，否则输出\u0026#34;NO\u0026#34; out.println(flag ? \u0026#34;YES\u0026#34; : \u0026#34;NO\u0026#34;); // 刷新输出缓冲区，确保所有输出都被写入 out.flush(); // 关闭输出流 out.close(); // 关闭输入流 br.close(); } ","date":"2026-03-09T20:51:45Z","permalink":"https://iamxurulin.github.io/p/bishi100-%E6%A8%A1%E6%9D%BF%E4%BA%8C%E5%88%86%E5%9B%BE%E7%BB%93%E6%9E%84-a-%E6%9F%93%E8%89%B2%E5%88%A4%E5%AE%9Adfs/","title":"BISHI100 【模板】二分图结构Ⅰ-A ‖ 染色判定：DFS"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 public static void main(String[] args) throws IOException { // 使用 BufferedReader 和 PrintWriter 提升大规模数据下的 I/O 效率 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 1. 读取成员数 n 和 好友关系数 m String[] strA = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int n = Integer.parseInt(strA[0]); int m = Integer.parseInt(strA[1]); // nameToID: 将字符串姓名映射为整数 ID，方便作为数组下标处理 HashMap\u0026lt;String, Integer\u0026gt; nameToID = new HashMap\u0026lt;\u0026gt;(); // idToName: 将整数 ID 映射回姓名，用于最后的结果输出 String[] idToName = new String[n]; // adj: 邻接表，存储每个人的好友列表 List\u0026lt;Integer\u0026gt;[] adj = new ArrayList[n]; // deg: 存储每个人的“社牛指数”（即节点的度数） int[] deg = new int[n]; int idCount = 0; // 记录当前已分配的 ID 数量 // 2. 构建图结构 for (int i = 0; i \u0026lt; m; i++) { String[] pair = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); // 处理每一对好友关系中的两个姓名 for (String name : pair) { // 如果是第一次见到的姓名，分配一个新的 ID 并初始化其邻接表 if (!nameToID.containsKey(name)) { idToName[idCount] = name; adj[idCount] = new ArrayList\u0026lt;\u0026gt;(); nameToID.put(name, idCount++); } } // 获取两个好友对应的 ID int u = nameToID.get(pair[0]); int v = nameToID.get(pair[1]); // 无向图：双方互相添加进对方的好友列表 adj[u].add(v); adj[v].add(u); // 双方的度数各加 1 deg[u]++; deg[v]++; } // 3. 判定“社牛”成员 List\u0026lt;String\u0026gt; res = new ArrayList\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; idCount; i++) { // 孤立点（没有好友的人）不符合社牛定义 if (deg[i] == 0) { continue; } // sum: 累计该成员所有好友的度数总和 long sum = 0; for (int neighbor : adj[i]) { sum += deg[neighbor]; } /** * 判定公式推导： * 原条件：deg(x) \u0026gt; (sum_of_friends_deg / deg(x)) * 变形得：deg(x) * deg(x) \u0026gt; sum_of_friends_deg * * 优点：避免浮点数除法导致的精度丢失。 * 注意：deg 最大为 10^5，平方后达 10^10，超过了 int 范围，必须强转为 long 计算。 */ if ((long) deg[i] * deg[i] \u0026gt; sum) { res.add(idToName[i]); } } // 4. 输出处理 if (res.isEmpty()) { // 若不存在任何社牛，输出 None out.println(\u0026#34;None\u0026#34;); } else { // 题目要求按字典序升序输出 Collections.sort(res); for (int i = 0; i \u0026lt; res.size(); i++) { // 格式化输出：姓名之间用单个空格分隔 out.print(res.get(i) + (i == res.size() - 1 ? \u0026#34;\u0026#34; : \u0026#34; \u0026#34;)); } out.println(); } // 5. 刷新缓冲区并关闭资源 out.flush(); out.close(); br.close(); } ","date":"2026-03-08T21:13:49Z","permalink":"https://iamxurulin.github.io/p/bishi99-%E6%88%91%E6%9C%8B%E5%8F%8B%E7%9A%84%E6%9C%8B%E5%8F%8B%E4%B8%8D%E6%98%AF%E6%88%91%E7%9A%84%E6%9C%8B%E5%8F%8B/","title":"BISHI99 我朋友的朋友不是我的朋友"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 public static void main(String[] args) throws IOException { // 使用 BufferedReader 和 PrintWriter 提升大规模数据下的 I/O 效率 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 1. 读取学生总人数 n int n = Integer.parseInt(br.readLine().trim()); // 2. 读取指认关系字符串并按空格拆分 String[] strA = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); // p[i] 存储学生 i 指认的学生编号 // 使用 n+1 大小以便让下标 1~n 对应学生编号 1~n，符合直觉 int[] p = new int[n + 1]; for (int i = 1; i \u0026lt;= n; i++) { p[i] = Integer.parseInt(strA[i - 1]); } // 3. 题目要求：假设老师最初抓到的是第 i 个学生 // 我们需要对 1 到 n 每一个起点都模拟一遍追踪过程 for (int i = 1; i \u0026lt;= n; i++) { // 获取从起点 i 开始追踪，最后被老师劝退的学生编号 int res = findStu(i, p, n); // 格式化输出：除了最后一个数，后面都加空格 out.print(res + (i == n ? \u0026#34;\u0026#34; : \u0026#34; \u0026#34;)); } // 4. 刷新缓冲区确保数据完整写出，并按规范关闭流资源 out.flush(); out.close(); br.close(); } /** * 追踪退学警告的过程 * * @param start 老师最初抓到的学生编号 * @param p 指认关系数组（图的邻接关系） * @param n 学生总数 * @return 第一个收到两次警告（即导致退学）的学生编号 */ private static int findStu(int start, int[] p, int n) { // visited[i] 记录学生 i 是否已经收到过一次警告 // 每一轮新起点都需要一个全新的标记数组 boolean[] visited = new boolean[n + 1]; int cur = start; // 从老师抓到的第一位学生开始 // 由于学生数量有限且出度为 1，追踪过程必然会进入一个环并死循环 // 我们通过 while(true) 配合内部 return 退出 while (true) { // 如果当前学生 cur 的 visited 标记已经是 true， // 说明这是他第二次被抓到了，触发“劝说退学”逻辑，直接返回他 if (visited[cur]) { return cur; } // 第一次抓到该学生，打上标记（发出第一次退学警告） visited[cur] = true; // 根据供述，抓取下一个被指认的人 cur = p[cur]; } } ","date":"2026-03-08T20:21:01Z","permalink":"https://iamxurulin.github.io/p/bishi98-%E8%B0%8D%E4%B8%AD%E8%B0%8D%E4%B8%AD%E8%B0%8D%E4%B8%AD%E8%B0%8D%E4%B8%AD%E8%B0%8D./","title":"BISHI98 谍中谍中谍中谍中谍."},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 public static void main(String[] args) throws IOException { // 使用 BufferedReader 和 PrintWriter 提升大规模数据下的 I/O 效率 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取第一行输入，获取节点数n和边数m String[] strA = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int n = Integer.parseInt(strA[0]); // 节点数量 int m = Integer.parseInt(strA[1]); // 边的数量 int[] t = new int[n + 1]; // 存储每个节点的类型，索引从1开始 // 读取第二行输入，获取每个节点的类型 String[] strB = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); // 将节点类型数据存入数组t，索引从1开始 for (int i = 1; i \u0026lt;= n; i++) { t[i] = Integer.parseInt(strB[i - 1]); } // 创建邻接表表示的图结构 List\u0026lt;Integer\u0026gt;[] adj = new ArrayList[n + 1]; for (int i = 1; i \u0026lt;= n; i++) { adj[i] = new ArrayList\u0026lt;\u0026gt;(); } // 读取所有边的信息，构建无向图 for (int i = 0; i \u0026lt; m; i++) { String[] edge = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int u = Integer.parseInt(edge[0]); // 边的一个端点 int v = Integer.parseInt(edge[1]); // 边的另一个端点 adj[u].add(v); // 添加边到邻接表 adj[v].add(u); // 无向图，双向添加 } // 检查起点或终点是否为类型1，如果是则直接输出\u0026#34;No\u0026#34; if (t[1] == 1 || t[n] == 1) { out.println(\u0026#34;No\u0026#34;); } else { // 调用BFS算法判断路径是否可行 if (bfs(n, adj, t)) { out.println(\u0026#34;Yes\u0026#34;); } else { out.println(\u0026#34;No\u0026#34;); } } // 刷新缓冲区确保数据完整写出并关闭流 out.flush(); out.close(); br.close(); } /** * 使用广度优先搜索(BFS)算法判断从节点1到节点n是否存在有效路径 * * @param n 节点总数 * @param adj 邻接表，表示图的连接关系 * @param t 数组，标记节点的状态(0表示可通过，非0表示不可通过) * @return 如果存在从节点1到节点n的有效路径则返回true，否则返回false */ private static boolean bfs(int n, List\u0026lt;Integer\u0026gt;[] adj, int[] t) { // 创建队列用于BFS遍历 Queue\u0026lt;Integer\u0026gt; queue = new LinkedList\u0026lt;\u0026gt;(); // 创建访问标记数组，记录节点是否被访问过 boolean[] visited = new boolean[n + 1]; // 从节点1开始搜索 queue.add(1); visited[1] = true; // 当队列不为空时，继续搜索 while (!queue.isEmpty()) { // 取出队首节点 int cur = queue.poll(); // 如果到达目标节点n，返回true if (cur == n) { return true; } // 遍历当前节点的所有邻接节点 for (int neighbor : adj[cur]) { // 如果邻接节点可通过且未被访问过 if (t[neighbor] == 0 \u0026amp;\u0026amp; !visited[neighbor]) { // 标记为已访问并入队 visited[neighbor] = true; queue.add(neighbor); } } } // 队列已空未到达目标节点，返回false return false; } ","date":"2026-03-07T20:12:40Z","permalink":"https://iamxurulin.github.io/p/bishi97%E6%97%BA%E4%BB%94%E5%93%A5%E5%93%A5%E8%B5%B0%E8%BF%B7%E5%AE%AB/","title":"BISHI97旺仔哥哥走迷宫"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 // 使用数组模拟指针，lc[i] 存储节点 i 的左孩子，rc[i] 存储右孩子 private static int[] lc, rc; // 使用 StringBuilder 缓存结果 private static StringBuilder preOrder = new StringBuilder(); private static StringBuilder inOrder = new StringBuilder(); private static StringBuilder postOrder = new StringBuilder(); public static void main(String[] args) throws IOException { // 使用 BufferedReader 和 PrintWriter 提升大规模数据下的 I/O 效率 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int n = Integer.parseInt(br.readLine().trim()); // 边界情况：空树 if (n == 0) { return; } // 初始化结构：n+1 长度是为了匹配从 1 开始的节点编号 lc = new int[n + 1]; rc = new int[n + 1]; boolean[] hasParent = new boolean[n + 1]; // 用于寻找根节点（根节点没有父节点） // 使用邻接表暂时存储“父 -\u0026gt; 子”的原始指向关系，随后再按规则分配左右 List\u0026lt;Integer\u0026gt;[] adj = new ArrayList[n + 1]; for (int i = 1; i \u0026lt;= n; i++) { adj[i] = new ArrayList\u0026lt;\u0026gt;(); } // 读取 n-1 条边，构建树的层级关系 for (int i = 0; i \u0026lt; n - 1; i++) { String[] edge = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int u = Integer.parseInt(edge[0]); // 父节点 int v = Integer.parseInt(edge[1]); // 子节点 adj[u].add(v); hasParent[v] = true; // 只要被指向，就说明该节点不是根节点 } /** * 根据题目定义的规则，将 adj 里的子节点分配到 lc 或 rc */ for (int i = 1; i \u0026lt;= n; i++) { // 父节点拥有两个孩子 if (adj[i].size() == 2) { int c1 = adj[i].get(0); int c2 = adj[i].get(1); // 编号小者为左，大者为右 lc[i] = Math.min(c1, c2); rc[i] = Math.max(c1, c2); } // 父节点仅有一个孩子 else if (adj[i].size() == 1) { int child = adj[i].get(0); // 子节点编号 \u0026gt; 父节点编号 ➜ 视为左孩子；反之 ➜ 视为右孩子 if (child \u0026gt; i) { lc[i] = child; } else { rc[i] = child; } } } // 寻找整棵树的唯一根节点 int root = 1; for (int i = 1; i \u0026lt;= n; i++) { if (!hasParent[i]) { root = i; break; } } // 从根节点开始DFS traverse(root); // 格式化输出 out.println(preOrder.toString().trim()); out.println(inOrder.toString().trim()); out.println(postOrder.toString().trim()); // 刷新缓冲区确保数据完整写出并关闭流 out.flush(); out.close(); br.close(); } /** * 深度优先遍历 (DFS) 方法 * * @param cur 当前正在访问的节点索引 */ private static void traverse(int cur) { // 基准情形：如果当前节点为 0，说明触碰到了空叶子节点，停止向下延伸 if (cur == 0) { return; } /** * 1. 先序遍历 (Pre-order)：[中] -\u0026gt; 左 -\u0026gt; 右 * 刚进入当前节点时立即记录。 */ preOrder.append(cur).append(\u0026#34; \u0026#34;); // 向左延伸 traverse(lc[cur]); /** * 2. 中序遍历 (In-order)：左 -\u0026gt; [中] -\u0026gt; 右 * 当左子树全部处理完毕回溯到当前点时记录。 */ inOrder.append(cur).append(\u0026#34; \u0026#34;); // 向右延伸 traverse(rc[cur]); /** * 3. 后序遍历 (Post-order)：左 -\u0026gt; 右 -\u0026gt; [中] * 当左右子树全部处理完毕，即将彻底离开当前点时记录。 */ postOrder.append(cur).append(\u0026#34; \u0026#34;); } ","date":"2026-03-07T19:38:45Z","permalink":"https://iamxurulin.github.io/p/bishi96-%E5%85%88%E5%BA%8F%E9%81%8D%E5%8E%86%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%E5%92%8C%E5%90%8E%E5%BA%8F%E9%81%8D%E5%8E%86/","title":"BISHI96 先序遍历、中序遍历和后序遍历"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，PrintWriter输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取第一行输入，分割字符串并转换为整数n和m String[] strA = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int n = Integer.parseInt(strA[0]); // 节点数 int m = Integer.parseInt(strA[1]); // 边数 // 创建邻接表，大小为n+1（节点从1开始编号） List\u0026lt;Integer\u0026gt;[] adj = new ArrayList[n + 1]; // 初始化每个节点的邻接表 for (int i = 1; i \u0026lt;= n; i++) { adj[i] = new ArrayList\u0026lt;\u0026gt;(); } // 读取m条边，构建无向图的邻接表 for (int i = 0; i \u0026lt; m; i++) { // 读取边的两个端点 String[] edge = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int u = Integer.parseInt(edge[0]); // 边的一个端点 int v = Integer.parseInt(edge[1]); // 边的另一个端点 // 无向图，两个方向都添加 adj[u].add(v); adj[v].add(u); } // 遍历每个节点，输出其邻接节点 for (int i = 1; i \u0026lt;= n; i++) { // 如果节点没有邻接节点，输出\u0026#34;None\u0026#34; if (adj[i].isEmpty()) { out.println(\u0026#34;None\u0026#34;); } else { // 对邻接节点进行排序 Collections.sort(adj[i]); // 输出排序后的邻接节点 for (int j = 0; j \u0026lt; adj[i].size(); j++) { // 如果是最后一个节点，直接输出，否则输出空格 out.print(adj[i].get(j) + (j == adj[i].size() - 1 ? \u0026#34;\u0026#34; : \u0026#34; \u0026#34;)); } out.println(); } } // 刷新缓冲区并释放资源 out.flush(); out.close(); br.close(); } ","date":"2026-03-06T21:04:50Z","permalink":"https://iamxurulin.github.io/p/bishi95-%E6%A8%A1%E6%9D%BF%E9%93%BE%E5%BC%8F%E5%89%8D%E5%90%91%E6%98%9F/","title":"BISHI95 【模板】链式前向星"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，PrintWriter输出结果，提高IO效率 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取输入字符串并去除首尾空格 String s = br.readLine().trim(); // 处理空字符串或null值的情况 if (s == null || s.length() == 0) { out.println(0); // 输出0 out.flush(); // 确保所有输出都被刷新 return; // 提前返回 } // 调用manacher算法处理字符串并输出结果 out.println(manacher(s)); out.flush(); // 刷新输出缓冲区 out.close(); // 关闭输出流 br.close(); // 关闭输入流 } /** * Manacher算法实现，用于查找字符串中的最长回文子串的长度 * 该算法通过预处理字符串和利用回文对称性来高效地找到最长回文子串 * * @param s 输入的字符串 * @return 返回最长回文子串的长度 */ private static int manacher(String s) { // 构造新的字符串，以^开头，$结尾，并在每个字符间插入#，这样可以统一处理奇数和偶数长度的回文 StringBuilder sb = new StringBuilder(\u0026#34;^\u0026#34;); // 在原字符串的每个字符前后添加特殊字符# for (char c : s.toCharArray()) { sb.append(\u0026#34;#\u0026#34;).append(c); } // 添加结尾标记 sb.append(\u0026#34;#$\u0026#34;); // 将构造好的字符串转换为字符数组 char[] t = sb.toString().toCharArray(); // 获取新字符串的长度 int n = t.length; // 创建数组p，p[i]表示以t[i]为中心的最长回文半径 int[] p = new int[n]; // c表示当前回文中心，r表示当前回文右边界 int c = 0, r = 0; // 记录最长回文半径 int maxLen = 0; // 遍历字符串，跳过首尾的特殊字符 for (int i = 1; i \u0026lt; n - 1; i++) { // 利用回文对称性，如果i在当前回文范围内，则获取其对称位置的回文半径 p[i] = r \u0026gt; i ? Math.min(r - i, p[2 * c - i]) : 0; // 尝试扩展以i为中心的回文 while (t[i + p[i] + 1] == t[i - p[i] - 1]) { p[i]++; } // 如果当前回文右边界超过了r，更新中心和右边界 if (i + p[i] \u0026gt; r) { c = i; r = i + p[i]; } // 更新最大回文半径 maxLen = Math.max(maxLen, p[i]); } // 返回最长回文半径，也就是最长回文子串的长度 return maxLen; } ","date":"2026-03-06T20:32:16Z","permalink":"https://iamxurulin.github.io/p/bishi94-%E6%A8%A1%E6%9D%BF%E9%A9%AC%E6%8B%89%E8%BD%A6%E7%AE%97%E6%B3%95/","title":"BISHI94 【模板】马拉车算法"},{"content":" 思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 /** * trie[p][i]: 字典树的节点存储。 * 第一维 p 代表节点的编号（行号）； * 第二维 i 代表字符的映射索引（0-51）。 * 值存储的是该字符指向的下一个节点的编号。 */ private static int[][] trie = new int[1000005][52]; /** * count[p]: 前缀统计的核心。 * 记录有多少个模式串在插入过程中经过了编号为 p 的节点。 * 因为我们要统计“以某串为前缀的个数”，所以每经过一个点都要加1。 */ private static int[] count = new int[1000005]; /** * nodes: 记录当前字典树已经创建的节点总数。 * 编号 0 始终代表根节点（不存储具体字符）。 */ private static int nodes = 0; public static void main(String[] args) throws IOException { // 使用高效的输入输出流处理百万级数据，防止超时 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取第一行：模式串数量 n 和查询次数 q String[] strA = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int n = Integer.parseInt(strA[0]); int q = Integer.parseInt(strA[1]); // 1. 构建字典树阶段 for (int i = 0; i \u0026lt; n; i++) { insert(br.readLine().trim()); } // 2. 查询阶段 for (int i = 0; i \u0026lt; q; i++) { // 直接输出以该文本串为前缀的模式串数量 out.println(query(br.readLine().trim())); } // 刷新缓冲区并释放资源 out.flush(); out.close(); br.close(); } /** * 将字符映射到 0-51 的索引空间。 * 区分大小写：\u0026#39;A\u0026#39;-\u0026#39;Z\u0026#39; 映射到 0-25，\u0026#39;a\u0026#39;-\u0026#39;z\u0026#39; 映射到 26-51。 */ private static int getIndex(char c) { return c \u0026gt;= \u0026#39;A\u0026#39; \u0026amp;\u0026amp; c \u0026lt;= \u0026#39;Z\u0026#39; ? c - \u0026#39;A\u0026#39; : c - \u0026#39;a\u0026#39; + 26; } /** * 模式串插入方法 * 核心逻辑：沿着路径在每个经过的节点打标记计数。 */ private static void insert(String s) { int p = 0; // 从根节点 (编号 0) 开始 for (char c : s.toCharArray()) { int i = getIndex(c); // 如果当前字符在当前节点下没有对应的子节点，则新建一个 if (trie[p][i] == 0) { trie[p][i] = ++nodes; } // 移动到子节点 p = trie[p][i]; /** * 关键：只要经过这个节点，count 就自增。 * 这样 count[p] 就代表了有多少个单词是以“从根到当前点”这一串字符为前缀的。 */ count[p]++; } } /** * 文本串查询方法 * 核心逻辑：在字典树中顺着文本串寻找路径，若能走完路径，返回最后的 count。 */ private static int query(String t) { int p = 0; // 从根节点开始 for (char c : t.toCharArray()) { int i = getIndex(c); /** * 如果在路径中发现某个字符没有对应的分支， * 说明没有任何一个模式串包含这个前缀，直接返回 0。 */ if (trie[p][i] == 0) { return 0; } p = trie[p][i]; // 顺着路径向下走 } /** * 成功走完文本串 t 的所有字符， * 此时节点 p 的 count 值就是在插入阶段经过该点的字符串总数。 */ return count[p]; } ","date":"2026-03-05T22:07:17Z","permalink":"https://iamxurulin.github.io/p/bishi93-%E6%A8%A1%E6%9D%BFtrie-%E5%AD%97%E5%85%B8%E6%A0%91/","title":"BISHI93 【模板】Trie 字典树"},{"content":" 思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 public static void main(String[] args) throws IOException { // 创建缓冲读取器，用于从标准输入读取数据 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // 创建打印输出器，用于向标准输出打印数据 PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取测试用例的数量T int T = Integer.parseInt(br.readLine().trim()); // 处理每个测试用例 while (T-- \u0026gt; 0) { // 读取一行输入并分割成字符串数组 String[] s = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); // 获取字符串的长度n int n = Integer.parseInt(s[0]); // 计算前缀函数数组 int[] pi = computePre(s[1], n); // 输出前缀函数数组，元素间用空格分隔 for (int i = 0; i \u0026lt; n; i++) { out.print(pi[i] + (i == n - 1 ? \u0026#34;\u0026#34; : \u0026#34; \u0026#34;)); } // 换行 out.println(); } // 刷新输出缓冲区，确保所有输出都被写出 out.flush(); // 关闭输出流 out.close(); // 关闭输入流 br.close(); } /** * 计算字符串的前缀函数（Prefix Function） * 前缀函数pi[i]表示字符串s[0..i]的最长相等前后缀的长度（不包括s[0..i]本身） * * @param s 输入的字符串 * @param n 字符串的长度 * @return 返回前缀函数数组pi，其中pi[i]表示s[0..i]的最长相等前后缀长度 */ private static int[] computePre(String s, int n) { int[] pi = new int[n]; // 初始化前缀函数数组，长度为n // 从第二个字符开始遍历字符串 for (int i = 1; i \u0026lt; n; i++) { int j = pi[i - 1]; // 初始化j为前一个字符的前缀函数值 // 当j大于0且当前字符不匹配时，通过前缀函数回退 while (j \u0026gt; 0 \u0026amp;\u0026amp; s.charAt(i) != s.charAt(j)) { j = pi[j - 1]; // 回退到前一个可能匹配的位置 } // 如果当前字符匹配，则增加匹配长度 if (s.charAt(i) == s.charAt(j)) { j++; } // 将当前字符的前缀函数值存入数组 pi[i] = j; } return pi; // 返回计算得到的前缀函数数组 } ","date":"2026-03-05T20:58:37Z","permalink":"https://iamxurulin.github.io/p/bishi92-%E6%A8%A1%E6%9D%BF%E5%89%8D%E7%BC%80%E5%87%BD%E6%95%B0kmp/","title":"BISHI92 【模板】前缀函数（kmp）"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 // n: 小木棒总数, sum: 所有木棒总长度, targetL: 目标原始长度, numbers: 目标原始木棒根数 private static int n, sum, targetL, numbers; private static Integer[] a; // 存储砍断后的小木棒长度 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); n = Integer.parseInt(br.readLine().trim()); String[] s = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); a = new Integer[n]; boolean[] used = new boolean[n]; // 标记每根小木棒是否已被乔治使用 sum = 0; int maxL = 0; for (int i = 0; i \u0026lt; n; i++) { a[i] = Integer.parseInt(s[i]); sum += a[i]; maxL = Math.max(maxL, a[i]); // 原始长度至少要等于最长的那根小木棒 } // 【剪枝优化 1】：降序排序。 // 优先尝试长木棒。因为长木棒对位置要求更苛刻，如果它拼不通，能更早触发回溯。 Arrays.sort(a, (x1, x2) -\u0026gt; (x2 - x1)); // 枚举原始木棒可能的长度 targetL // 范围：从最长的那根开始，直到所有木棒的总和 for (targetL = maxL; targetL \u0026lt;= sum; targetL++) { // 【条件约束】：原始长度必须能被总长度整除，否则乔治不可能拼成同样长的木棒 if (sum % targetL != 0) { continue; } numbers = sum / targetL; // 计算在当前 targetL 下应该拼出多少根 // 尝试拼凑，初始：已完成0根，当前长度0，从第0个小木棒开始找 if (dfs(0, 0, 0, used)) { out.println(targetL); // 找到第一个可行的最小长度，即为答案 break; } } out.flush(); out.close(); br.close(); } /** * DFS 拼凑过程 * * @param completed 已完整拼好的原始木棒根数 * @param currentL 当前正在拼的那根原始木棒已经达到的长度 * @param idx 为了避免重复组合，从数组的哪一个下标开始挑选下一根小木棒 * @param used 使用情况记录 */ private static boolean dfs(int completed, int currentL, int idx, boolean[] used) { // 【递归出口】：如果乔治拼好了所有原始木棒，大功告成 if (completed == numbers) { return true; } // 如果当前这根拼满了，开启下一根木棒的拼凑 if (currentL == targetL) { return dfs(completed + 1, 0, 0, used); } for (int i = idx; i \u0026lt; n; i++) { // 如果这根用过了，或者放进去就超过了目标长度，跳过 if (used[i] || currentL + a[i] \u0026gt; targetL) { continue; } // 【做选择】：尝试放这根木棒 used[i] = true; if (dfs(completed, currentL + a[i], i + 1, used)) { return true; } // 【撤销选择】：刚才的尝试失败了，拿出来 used[i] = false; // --- 核心剪枝策略 (极其关键) --- // 【剪枝优化 2】：首棒失败判定 // 如果此时 currentL 为 0，说明我们正在尝试拼一根新木棒的第一部分。 // 如果第一部分用这根最长的木棒都拼不出来，那后面更拼不出来了。 // 【剪枝优化 3】：末棒失败判定 // 如果加上这根木棒刚好填满了 targetL，但在后续递归中失败了， // 说明用这根刚好填满的方案不行，那么换用几根更碎的小木棒来填这个坑也肯定不行。 if (currentL == 0 || currentL + a[i] == targetL) { return false; } // 【剪枝优化 4】：去重剪枝 // 如果当前长度的木棒不行，那后面相同长度的木棒也肯定不行，直接跳过。 while (i + 1 \u0026lt; n \u0026amp;\u0026amp; a[i + 1].equals(a[i])) { i++; } } return false; } ","date":"2026-03-04T22:20:09Z","permalink":"https://iamxurulin.github.io/p/dfs-%E5%89%AA%E6%9E%9Dbishi91-%E6%8B%BC%E6%8E%A5%E6%9C%A8%E6%A3%8D/","title":"【DFS+剪枝】BISHI91 拼接木棍"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 private static final int MOD = 1000000007; private static long[][][] memory = new long[101][101][101]; public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，使用PrintWriter输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int T = Integer.parseInt(br.readLine().trim()); while (T-- \u0026gt; 0) { String[] s = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int a = Integer.parseInt(s[0]); int b = Integer.parseInt(s[1]); int c = Integer.parseInt(s[2]); out.println(solve(a, b, c)); } // 刷新输出流，确保所有输出都被写入 out.flush(); // 关闭输出流，释放资源 out.close(); // 关闭输入流，释放资源 br.close(); } /** * 这是一个递归求解函数，使用记忆化技术来优化性能 * * @param a 第一个参数 * @param b 第二个参数 * @param c 第三个参数 * @return 返回计算结果，类型为long */ private static long solve(int a, int b, int c) { // 如果任一参数小于等于0，返回1 if (a \u0026lt;= 0 || b \u0026lt;= 0 || c \u0026lt;= 0) { return 1; } // 如果结果已经计算过并存储在memory中，直接返回存储的结果 if (memory[a][b][c] != 0) { return memory[a][b][c]; } long res; // 用于存储计算结果 // 当a\u0026lt;b且b\u0026lt;c时，使用特定的递归公式计算 if (a \u0026lt; b \u0026amp;\u0026amp; b \u0026lt; c) { res = (solve(a, b, c - 1) + solve(a, b - 1, c - 1)) % MOD; res = (res - solve(a, b - 1, c) + MOD) % MOD; } else { res = (solve(a - 1, b, c) + solve(a - 1, b - 1, c)) % MOD; res = (res + solve(a - 1, b, c - 1)) % MOD; res = (res - solve(a - 1, b - 1, c - 1) + MOD) % MOD; } return memory[a][b][c] = res; } ","date":"2026-03-04T20:51:44Z","permalink":"https://iamxurulin.github.io/p/bishi90-%E6%A8%A1%E6%9D%BF%E8%AE%B0%E5%BF%86%E5%8C%96%E6%90%9C%E7%B4%A2/","title":"BISHI90 【模板】记忆化搜索"},{"content":"开发个人项目时，想让他人访问往往需要购买服务器、配置域名解析，成本高且流程繁琐。\n本文介绍一种零成本方案 —— 仅穿透前端即可实现内网个人项目的公网访问。\nngrok 账号注册与工具准备 首先在https://ngrok.com/ 官网注册一个账号，就能获得一个免费的dev结尾的域名。\n注册好之后，下载对应的zip压缩包\n在官网个人后台 / 仪表盘（Dashboard）可直接复制个人专属的 Authtoken。\n分框架适配配置 如果前端是用 Vite + React 的项目，需要在 vite.config.js 文件加上allowedHosts这一行代码：\n1 2 3 4 5 6 // vite.config.js export default defineConfig({ server: { allowedHosts: [\u0026#39;xxx.dev\u0026#39;] // ngrok 域名 } }) 如果前端是基于 Umi Max + Ant Design Pro 的项目，前端默认是跑在 localhost:8000（umi dev），则需要修改2个文件。\n1.config/config.ts\n在文件里加上如下 proxy 部分：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // config/config.ts export default defineConfig({ // ... 其他配置 ... proxy: { \u0026#39;/api/\u0026#39;: { target: \u0026#39;http://localhost:[项目后端的端口号]\u0026#39;, changeOrigin: true, // 根据后端实际路由决定是否 rewrite // 如果后端接口路径就是 /api/xxx 开头 → 不要 rewrite // 如果后端是 /user/xxx（无 /api 前缀） → 加下面这行 // pathRewrite: { \u0026#39;^/api\u0026#39;: \u0026#39;\u0026#39; }, }, }, // ... 其他配置 ... }); 这段只在 umi dev / npm run dev 时生效。\n2.src/app.tsx\n找到 request 配置，把 baseURL 相关全部注释或删除：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 // src/app.tsx export const request: RequestConfig = { // ！！！ 下面这整段注释掉或删除 ！！！ // baseURL: \u0026#34;http://localhost:[项目后端的端口号]\u0026#34;, // 保持这两个（ngrok 警告页绕过 + cookie 跨域） withCredentials: true, headers: { \u0026#39;ngrok-skip-browser-warning\u0026#39;: \u0026#39;true\u0026#39;, }, ...errorConfig, }; 项目启动与公网访问 接下来，依次启动后端和前端，将之前下载好的压缩包解压后直接双击 exe 即可打开命令行，执行以下命令：\n1 2 3 ngrok config add-authtoken [YourAuthtoken] ngrok http [项目前端的端口号] 复制 ngrok 生成的 https 地址，其他人就能够访问你的项目了。\n","date":"2026-03-04T17:39:43Z","permalink":"https://iamxurulin.github.io/p/%E9%9B%B6%E6%88%90%E6%9C%AC%E4%B8%8A%E7%BA%BF%E4%B8%AA%E4%BA%BA%E9%A1%B9%E7%9B%AE-ngrok-%E4%BB%85%E7%A9%BF%E9%80%8F%E5%89%8D%E7%AB%AF%E5%AE%9E%E7%8E%B0%E5%85%AC%E7%BD%91%E8%AE%BF%E9%97%AE/","title":"零成本上线个人项目 ——ngrok 仅穿透前端实现公网访问"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，使用PrintWriter输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取整数n，表示数组的长度 int n = Integer.parseInt(br.readLine().trim()); // 创建长度为n的数组P，用于存储输入的数值 long[] P = new long[n]; // 创建长度为n+1的前缀和数组preSum，preSum[0]=0 long[] preSum = new long[n+1]; // 读取一行输入，按空格分割成字符串数组 String[] s= br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); // 计算前缀和 for(int i=0;i\u0026lt;n;i++){ // 将字符串转换为长整型并存入数组P P[i]= Long.parseLong(s[i]); // 计算前缀和，preSum[i+1]表示前i个元素的和 preSum[i+1]=preSum[i]+P[i]; } // 初始化总和变量total long total = 0; // 遍历数组，从第2个元素开始到倒数第2个元素 for (int j=2;j\u0026lt;n;j++){ // 计算limit1，即(preSum[j]-1)/2 long limit1 = (preSum[j]-1)/2; // 计算limit2，即2*preSum[j]-preSum[n]-1 long limit2 = 2*preSum[j]-preSum[n]-1; // 取limit1和limit2中的较小值作为阈值 long threshold = Math.min(limit1,limit2); // 如果阈值大于等于第一个元素的前缀和 if (threshold \u0026gt;= preSum[1]){ // 调用upper函数计算符合条件的元素个数 int count = upper(preSum,1,j-1,threshold); // 将count累加到total中 total += count; } } // 输出最终结果 out.println(total); // 刷新输出流，确保所有输出都被写入 out.flush(); // 关闭输出流，释放资源 out.close(); // 关闭输入流，释放资源 br.close(); } /** * 在有序数组中查找最后一个小于等于目标值的元素的位置，并返回满足条件的元素个数 * @param preSum 有序数组，表示前缀和数组 * @param left 查找范围的左边界 * @param right 查找范围的右边界 * @param target 目标值 * @return 返回在[left, right]范围内小于等于target的元素个数 */ private static int upper(long[] preSum,int left,int right,long target){ int low = left,high = right; // 初始化查找范围的左右边界 int ans = 0; // 初始化答案变量 // 执行二分查找 while(low\u0026lt;=high){ int mid = low +((high-low)\u0026gt;\u0026gt;1); // 计算中间值，使用位运算优化替代除法 // 这种写法可以避免(low+high)可能导致的整数溢出问题 // 检查中间值是否满足条件 if(preSum[mid]\u0026lt;=target){ // 如果中间值小于等于目标值 ans = mid; // 更新答案为当前位置 low = mid+1; // 在右半部分继续查找 }else{ // 如果中间值大于目标值 high = mid-1; // 在左半部分继续查找 } } // 返回结果：如果找到满足条件的元素，返回其个数；否则返回0 return ans\u0026gt;0?(ans-left+1):0; } ","date":"2026-03-03T21:17:15Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%88%86bishi89-%E5%B1%B1%E5%B3%B0%E6%95%B0%E7%BB%84%E8%AE%A1%E6%95%B0/","title":"【二分】BISHI89 山峰数组计数"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，使用PrintWriter输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取第一行输入，分割为字符串数组 String[] strA = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int n = Integer.parseInt(strA[0]); // 获取字符串长度 int m = Integer.parseInt(strA[1]); // 获取操作次数 char[] c = new char[n]; // 创建字符数组，虽然在此代码中未使用 String s= br.readLine().trim(); // 待处理的字符串 int low = 0,high = n; // 二分查找的边界 int ans = n; // 初始化答案为字符串长度 // 执行二分查找 while(low\u0026lt;=high){ int mid = low +((high-low)\u0026gt;\u0026gt;1); // 计算中间值，使用位运算优化替代除法 // 检查中间值是否满足条件 if(check(mid,n,m,s)){ ans = mid; // 如果满足条件，更新答案 high = mid-1; // 尝试寻找更小的解 }else{ low = mid+1; // 如果不满足，尝试更大的值 } } // 输出最终结果 out.println(ans); // 刷新输出流，确保所有输出都被写入 out.flush(); // 关闭输出流，释放资源 out.close(); // 关闭输入流，释放资源 br.close(); } private static boolean check(int k,int n,int m,String s){ // 如果k为0，检查字符串中是否不包含\u0026#39;W\u0026#39; if(k==0){ return !s.contains(\u0026#34;W\u0026#34;); } int count = 0; // 记录操作次数 // 遍历字符串 for (int i=0;i\u0026lt;n;i++){ // 如果当前字符是\u0026#39;W\u0026#39;，增加操作计数 if(s.charAt(i)==\u0026#39;W\u0026#39;){ count++; // 如果操作次数超过m，返回false if(count\u0026gt;m){ return false; } // 跳过接下来的k-1个字符 i+=(k-1); } } // 检查总操作次数是否不超过m return count\u0026lt;=m; } ","date":"2026-03-03T20:41:07Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%88%86bishi88-%E5%B0%8F%E8%8B%AF%E7%9A%84%E9%AD%94%E6%B3%95%E6%9F%93%E8%89%B2/","title":"【二分】BISHI88 小苯的魔法染色"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，使用PrintWriter输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取第一行输入，分割为字符串数组 String[] strA = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int n = Integer.parseInt(strA[0]); long m = Long.parseLong(strA[1]); int[] c = new int[n]; // 创建大小为n的整型数组 // 读取第二行输入，分割为字符串数组 String[] strB = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); // 将字符串数组转换为整型数组 for(int i=0;i\u0026lt;n;i++){ c[i]=Integer.parseInt(strB[i]); } // 初始化二分查找的边界和结果变量 long low = 0,high = 10000000000L; // 设置查找范围为0到100亿 long ans = 0; // 执行二分查找 while(low\u0026lt;=high){ long mid = low +((high-low)\u0026gt;\u0026gt;1); // 计算中间值，使用位运算优化 // 检查中间值是否满足条件 if(check(mid,n,m,c)){ ans = mid; // 如果满足条件，更新答案 low = mid+1; // 尝试更大的值 }else{ high = mid-1; // 如果不满足，尝试更小的值 } } // 输出最终结果 out.println(ans); // 刷新输出流 out.flush(); // 关闭输出流 out.close(); // 关闭输入流 br.close(); } private static boolean check(long x,int n,long m,int[] c){ long need = 0; // 需要添加的资源总量 for(int count:c){ // 遍历每种资源的当前数量 if(count\u0026lt;x){ // 如果当前资源数量小于目标数量 need +=(x-count); // 计算需要添加的资源数量 } } return need \u0026lt;= m\u0026amp;\u0026amp; need \u0026lt;=x; // 判断需要添加的资源总量是否不超过m和x } ","date":"2026-03-02T21:44:34Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%88%86bishi87-cqoi2010%E6%89%91%E5%85%8B%E7%89%8C/","title":"【二分】BISHI87 [CQOI2010]扑克牌"},{"content":" 思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 /** * 表示一个点的静态内部类 * 包含两个属性：d2和v */ static class Point { // 平方距离属性，通常用于表示点与点之间的距离平方 long d2; // 权值属性 long v; Point(long d2, long v) { // 使用this关键字将参数值赋给类的成员变量 this.d2 = d2; this.v = v; } } public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，使用PrintWriter输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取第一行输入，分割为字符串数组 String[] strA = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); // 获取点的数量n int n = Integer.parseInt(strA[0]); // 获取所需的最小权值S long S = Long.parseLong(strA[1]); // 创建二维数组存储每个点的坐标和权值 long[][] a = new long[n][3]; // 创建Point数组，用于存储每个点到原点的距离平方和权值 Point[] points = new Point[n]; // 初始化总权值 long total = 0; // 读取每个点的数据并计算相关信息 for (int i = 0; i \u0026lt; n; i++) { // 读取一行输入并分割 String[] str = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); // 存储x坐标 a[i][0] = Long.parseLong(str[0]); // 存储y坐标 a[i][1] = Long.parseLong(str[1]); // 存储权值 a[i][2] = Long.parseLong(str[2]); // 创建Point对象，计算点到原点的距离平方 points[i] = new Point(a[i][0] * a[i][0] + a[i][1] * a[i][1], a[i][2]); // 累加总权值 total += a[i][2]; } // 如果总权值小于所需权值S，输出-1并返回 if (total \u0026lt; S) { out.println(\u0026#34;-1\u0026#34;); out.flush(); return; } // 对points数组进行排序，使用Lambda表达式作为比较器，按照d2属性值升序排序 Arrays.sort(points, (x1, x2) -\u0026gt; Long.compare(x1.d2, x2.d2)); // 创建前缀和数组preSum，长度为n long[] preSum = new long[n]; // 初始化前缀和数组的第一个元素为points[0]的v值 preSum[0] = points[0].v; // 计算前缀和数组，preSum[i]存储从points[0]到points[i]的v值之和 for (int i = 1; i \u0026lt; n; i++) { preSum[i] = preSum[i - 1] + points[i].v; } // 初始化二分查找的边界，low为0，high为n-1 int low = 0, high = n - 1; // 初始化index为n-1，用于存储满足条件的最小索引 int index = n - 1; // 使用二分查找算法找到满足preSum[mid] \u0026gt;= S的最小mid值 while (low \u0026lt;= high) { // 计算中间位置，使用位运算防止溢出 int mid = low + ((high - low) \u0026gt;\u0026gt; 1); // 如果当前前缀和大于等于S，则更新index并向左查找更小的值 if (preSum[mid] \u0026gt;= S) { index = mid; high = mid - 1; } else { // 否则向右查找 low = mid + 1; } } // 输出满足条件的点的d2值的平方根 out.println(Math.sqrt(points[index].d2)); // 刷新输出流 out.flush(); // 关闭输出流 out.close(); // 关闭输入流 br.close(); } ","date":"2026-03-02T20:59:32Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%88%86bishi86-%E5%9C%86%E8%A6%86%E7%9B%96/","title":"【二分】BISHI86 圆覆盖"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，PrintWriter输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取第一行输入，分割成字符串数组 String[] strA = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); // 解析数组长度n和查询次数q int n = Integer.parseInt(strA[0]); int q = Integer.parseInt(strA[1]); // 创建数组a并读取输入数据 int[] a = new int[n]; String[] strB = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); for (int i = 0; i \u0026lt; n; i++) { a[i] = Integer.parseInt(strB[i]); } // 对数组进行排序，为后续查询做准备 Arrays.sort(a); // 处理q次查询 while (q-- \u0026gt; 0) { // 读取每次查询的左右边界 String[] strC = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int l = Integer.parseInt(strC[0]); int r = Integer.parseInt(strC[1]); // 使用二分查找计算下界和上界 int left = lowerBound(a, l); int right = upperBound(a, r); // 输出结果：范围内的元素个数 out.println(right - left); } // 关闭IO资源 out.flush(); out.close(); br.close(); } /** * 二分查找下界函数 * * @param a 已排序的数组 * @param target 目标值 * @return 第一个大于等于target的元素的索引 */ private static int lowerBound(int[] a, int target) { // 初始化查找范围的 low 和 high 指针 // low 从数组的起始位置 0 开始 // high 从数组长度开始（不包括该位置） int low = 0, high = a.length; // 当 low 小于 high 时，继续查找 // 这是循环查找的条件，确保查找范围有效 while (low \u0026lt; high) { // 计算中间位置 mid // 使用位运算 (high-low)\u0026gt;\u0026gt;1 代替除法 2，提高效率 // 防止整数溢出，同时确保 mid 总是在 low 和 high 之间 int mid = low + ((high - low) \u0026gt;\u0026gt; 1); // 如果中间位置的元素大于等于目标值 // 说明目标值可能在左半部分，或者 mid 就是我们要找的位置 // 因此将 high 设置为 mid，继续在左半部分查找 if (a[mid] \u0026gt;= target) { high = mid; } else { // 如果中间位置的元素小于目标值 // 说明目标值一定在右半部分 // 因此将 low 设置为 mid+1，继续在右半部分查找 low = mid + 1; } } // 当循环结束时，low 指向第一个大于等于 target 的元素的位置 // 如果所有元素都小于 target，则返回数组长度 return low; } /** * 二分查找上界函数 * * @param a 已排序的数组 * @param target 目标值 * @return 第一个大于target的元素的索引 */ private static int upperBound(int[] a, int target) { // 初始化查找范围的左右边界，左边界为0，右边界为数组长度 int low = 0, high = a.length; // 当左边界小于右边界时，继续查找 while (low \u0026lt; high) { // 计算中间位置，使用位运算防止溢出 int mid = low + ((high - low) \u0026gt;\u0026gt; 1); // 如果中间位置的值大于目标值，则上界在左半部分 if (a[mid] \u0026gt; target) { high = mid; } else { // 否则上界在右半部分，且不包括中间位置 low = mid + 1; } } // 返回第一个大于target的元素的索引 return low; } ","date":"2026-03-01T21:36:42Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%88%86bishi85-%E6%A8%A1%E6%9D%BF%E6%95%B4%E6%95%B0%E5%9F%9F%E4%BA%8C%E5%88%86/","title":"【二分】BISHI85 【模板】整数域二分"},{"content":" 思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 // 定义一个静态数组dist，用于存储从数字10到其他数字的最小步数 private static int[] dist = new int[301]; public static void main(String[] args)throws IOException{ // 调用bfs方法计算最小步数 bfs(); // 初始化输入输出流 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取测试用例数量 int T = Integer.parseInt(br.readLine().trim()); // 处理每个测试用例 while (T-- \u0026gt;0){ // 读取一行输入并分割成字符串数组 String[] str = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); // 将字符串数组转换为四个整数 int a = Integer.parseInt(str[0]); int b = Integer.parseInt(str[1]); int c = Integer.parseInt(str[2]); int d = Integer.parseInt(str[3]); // 计算四个数字的最小步数之和 int ans = dist[a]+dist[b]+dist[c]+dist[d]; // 输出结果 out.println(ans); } // 刷新并关闭输出流 out.flush(); out.close(); // 关闭输入流 br.close(); } /** * 使用BFS算法计算从数字10到其他数字的最小步数 * 可以进行的操作包括：+1, -1, +10, -10, +100, -100 * 特殊情况：可以直接跳到数字10或300 */ private static void bfs(){ // 初始化dist数组，-1表示未访问 Arrays.fill(dist,-1); // 创建队列用于BFS遍历 Queue\u0026lt;Integer\u0026gt; queue = new LinkedList\u0026lt;\u0026gt;(); // 设置起点数字10的步数为0 dist[10]=0; // 将起点加入队列 queue.add(10); // 定义可能的操作数组 int[] ops = {1,-1,10,-10,100,-100}; // 当队列不为空时继续遍历 while (!queue.isEmpty()){ // 取出队首元素 int u = queue.poll(); // 创建邻居列表 List\u0026lt;Integer\u0026gt; neighbors = new ArrayList\u0026lt;\u0026gt;(); // 应用所有可能的操作生成邻居 for(int op:ops){ neighbors.add(u+op); } // 添加特殊邻居：10和300 neighbors.add(10); neighbors.add(300); // 遍历所有邻居 for(int v:neighbors){ // 如果邻居在有效范围内且未被访问过 if(v\u0026gt;=10\u0026amp;\u0026amp;v\u0026lt;=300\u0026amp;\u0026amp;dist[v]==-1){ // 更新步数 dist[v]=dist[u]+1; // 将邻居加入队列 queue.add(v); } } } } ","date":"2026-03-01T20:38:54Z","permalink":"https://iamxurulin.github.io/p/bfsbishi84-%E6%97%B6%E6%B4%A5%E9%A3%8E%E7%9A%84%E8%B5%84%E6%BA%90%E6%94%B6%E9%9B%86/","title":"【BFS】BISHI84 时津风的资源收集"},{"content":" 思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 private static List\u0026lt;int[]\u0026gt; path = new ArrayList\u0026lt;\u0026gt;(); /** * 主方法，处理输入输出并调用回溯算法 * * @param args 命令行参数 * @throws IOException 可能抛出的IO异常 */ public static void main(String[] args) throws IOException { // 创建输入输出缓冲流 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取第一行输入，分割并转换为高度h和宽度w String[] strA = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); // 解析网格的高度和宽度 int h = Integer.parseInt(strA[0]); int w = Integer.parseInt(strA[1]); // 创建网格数组和访问标记数组 int[][] grid = new int[h][w]; boolean[][] visited = new boolean[h][w]; // 读取网格数据 for (int i = 0; i \u0026lt; h; i++) { // 读取一行输入并分割 String[] str = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); // 填充网格数据 for (int j = 0; j \u0026lt; w; j++) { grid[i][j] = Integer.parseInt(str[j]); } } // 从起点(0,0)开始回溯 backtrack(grid, h, w, 0, 0, visited); // 输出路径 for (int[] pos : path) { out.println(\u0026#34;(\u0026#34; + pos[0] + \u0026#34;,\u0026#34; + pos[1] + \u0026#34;)\u0026#34;); } // 刷新并关闭输出流和输入流 out.flush(); out.close(); br.close(); } /** * 使用回溯算法在网格中寻找路径 * * @return 如果找到从起点到终点的路径则返回true，否则返回false */ private static boolean backtrack(int[][] grid, int h, int w, int i, int j, boolean[][] visted) { // 检查当前位置是否有效：是否在网格范围内、是否是障碍物、是否已经访问过 if (i \u0026lt; 0 || i \u0026gt;= h || j \u0026lt; 0 || j \u0026gt;= w || grid[i][j] == 1 || visted[i][j]) { return false; } // 标记当前位置为已访问，并将当前位置添加到路径中 visted[i][j] = true; path.add(new int[]{i, j}); // 如果当前位置是终点，则返回true if (i == h - 1 \u0026amp;\u0026amp; j == w - 1) { return true; } // 定义四个方向：上、下、左、右 int[] dx = {-1, 1, 0, 0}; int[] dy = {0, 0, -1, 1}; // 尝试向四个方向移动 for (int k = 0; k \u0026lt; 4; k++) { // 递归调用backtrack方法，如果找到路径则返回true if (backtrack(grid, h, w, i + dx[k], j + dy[k], visted)) { return true; } } // 如果四个方向都无法继续前进，则回溯：从路径中移除当前位置 path.remove(path.size() - 1); return false; } ","date":"2026-02-28T22:45:36Z","permalink":"https://iamxurulin.github.io/p/%E5%9B%9E%E6%BA%AFbishi83-%E8%BF%B7%E5%AE%AB%E9%97%AE%E9%A2%98/","title":"【回溯】BISHI83 迷宫问题"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 /** * 主方法：处理吴铭市地图数据并统计被完全淹没的区域数量 * * 业务逻辑： * 1. 读取 N*N 的城市地图。 * 2. 通过遍历寻找每一个独立的“空地区域”（由 \u0026#39;#\u0026#39; 组成的四连通块）。 * 3. 对每个区域进行洪水上涨模拟判定。 * * @param args 命令行参数 * @throws IOException IO异常处理 */ public static void main(String[] args) throws IOException { // 使用高效流读取地图数据与输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取地图尺寸 N int N = Integer.parseInt(br.readLine().trim()); // grid: 地图矩阵，\u0026#39;.\u0026#39; 为已淹没，\u0026#39;#\u0026#39; 为空地 char[][] grid = new char[N][N]; // visit: 标记当前空地是否已被抗灾委员会统计过 boolean[][] visit = new boolean[N][N]; // 加载吴铭市当前受灾地图 for (int i = 0; i \u0026lt; N; i++) { grid[i] = br.readLine().trim().toCharArray(); } // ans: 记录洪水上涨一天后，完全消失的空地区域总数 int ans = 0; // 逐格扫描地图 for (int i = 0; i \u0026lt; N; i++) { for (int j = 0; j \u0026lt; N; j++) { // 发现一块新的且未统计过的空地区域 if (grid[i][j] == \u0026#39;#\u0026#39; \u0026amp;\u0026amp; !visit[i][j]) { /** * 执行 BFS 模拟： * 若该区域内所有格子在明天都会被淹没（即 bfs 返回 false）， * 则该区域属于“完全淹没区域”，计数加一。 */ if (!bfs(grid, N, i, j, visit)) { ans++; } } } } // 输出统计结果并释放资源 out.println(ans); out.flush(); out.close(); br.close(); } /** * 使用广度优先搜索 (BFS) 判定一个空地区域在洪水上涨后是否能“存活” * * 判定核心： * 一个区域能存活，当且仅当该区域内【至少有一个】格子是安全的。 * 安全格子的定义：其上下左右四个相邻方向全部都是空地 \u0026#39;#\u0026#39;，洪水无法直接上涨淹没它。 * * @param grid 城市地图矩阵 * @param N 地图尺寸 * @param i 区域起始点行坐标 * @param j 区域起始点列坐标 * @param visit 访问标记数组 * @return 如果该区域上涨后仍有残留（不完全消失）返回 true，否则返回 false */ private static boolean bfs(char[][] grid, int N, int i, int j, boolean[][] visit) { Queue\u0026lt;int[]\u0026gt; queue = new LinkedList\u0026lt;\u0026gt;(); // 将当前区域的起点加入队列并标记 queue.add(new int[]{i, j}); visit[i][j] = true; // areaSurvived: 标识整个区域是否能存活（只要有一个格子不被淹，即为 true） boolean areaSurvived = false; // 四联通方向向量 int[] dx = {-1, 1, 0, 0}; int[] dy = {0, 0, -1, 1}; while (!queue.isEmpty()) { int[] cur = queue.poll(); int x = cur[0]; int y = cur[1]; // flag: 检查当前格子 (x, y) 是否为“避风港” // 默认假设它是安全的（四周无洪水 \u0026#39;.\u0026#39;） boolean flag = true; // 扫描当前格子的四周 for (int k = 0; k \u0026lt; 4; k++) { int nextX = x + dx[k]; int nextY = y + dy[k]; // 如果邻居越界或者是洪水 \u0026#39;.\u0026#39;，则当前格子明早会被淹没 if (nextX \u0026lt; 0 || nextX \u0026gt;= N || nextY \u0026lt; 0 || nextY \u0026gt;= N || grid[nextX][nextY] == \u0026#39;.\u0026#39;) { flag = false; break; } } // 如果发现当前格子四周都是空地，它将保证该区域不被完全淹没 if (flag) { areaSurvived = true; } // 继续探索该区域的其他空地格子 for (int k = 0; k \u0026lt; 4; k++) { int nextX = x + dx[k]; int nextY = y + dy[k]; // 确保在地图范围内，且是同一区域的未访问空地 if (nextX \u0026gt;= 0 \u0026amp;\u0026amp; nextX \u0026lt; N \u0026amp;\u0026amp; nextY \u0026gt;= 0 \u0026amp;\u0026amp; nextY \u0026lt; N \u0026amp;\u0026amp; grid[nextX][nextY] == \u0026#39;#\u0026#39; \u0026amp;\u0026amp; !visit[nextX][nextY]) { visit[nextX][nextY] = true; queue.add(new int[]{nextX, nextY}); } } } // 返回该区域是否完全消失 return areaSurvived; } ","date":"2026-02-28T21:17:35Z","permalink":"https://iamxurulin.github.io/p/bfsbishi82-%E6%B2%A1%E6%8C%A1%E4%BD%8F%E6%B4%AA%E6%B0%B4/","title":"【BFS】BISHI82 没挡住洪水"},{"content":" 思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 /** * 主方法，用于读取输入数据并处理网格问题 * * @param args 命令行参数 * @throws IOException 可能抛出的IO异常 */ public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入流 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // 使用PrintWriter输出结果 PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取第一行输入并分割成字符串数组 String[] str = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); // 解析网格的行数n int n = Integer.parseInt(str[0]); // 解析网格的列数m int m = Integer.parseInt(str[1]); // 创建n行m列的字符网格 char[][] grid = new char[n][m]; // 逐行读取网格数据 for (int i = 0; i \u0026lt; n; i++) { // 将每行字符串转换为字符数组存入网格 grid[i] = br.readLine().toCharArray(); } // 创建n行m列的访问标记数组，初始值都为false boolean[][] visit = new boolean[n][m]; // 初始化答案计数器 int ans = 0; // 遍历网格的每个单元格 for (int i = 0; i \u0026lt; n; i++) { for (int j = 0; j \u0026lt; m; j++) { // 如果当前单元格是\u0026#39;.\u0026#39;且未被访问过 if (grid[i][j] == \u0026#39;.\u0026#39; \u0026amp;\u0026amp; !visit[i][j]) { // 使用BFS方法处理，如果返回true则增加答案计数 if (bfs(grid, n, m, i, j, visit)) { ans++; } } } } // 输出最终结果 out.println(ans); // 刷新输出流 out.flush(); // 关闭输出流 out.close(); // 关闭输入流 br.close(); } /** * 使用广度优先搜索(BFS)算法遍历连通区域，并判断该连通区域是否为矩形 * * @param grid 二维字符网格，其中\u0026#39;.\u0026#39;表示可走区域 * @param n 网格的行数 * @param m 网格的列数 * @param i 起始点的行坐标 * @param j 起始点的列坐标 * @param visit 记录访问状态的二维数组 * @return 如果连通区域是完整的矩形则返回true，否则返回false */ private static boolean bfs(char[][] grid, int n, int m, int i, int j, boolean[][] visit) { // 使用队列进行BFS遍历 Queue\u0026lt;int[]\u0026gt; queue = new LinkedList\u0026lt;\u0026gt;(); queue.add(new int[]{i, j}); visit[i][j] = true; // 标记起始点为已访问 // 记录连通区域的最小和最大x、y坐标 int minX = i, maxX = i; int minY = j, maxY = j; // 记录连通区域中点的数量 int count = 0; // 定义四个方向的偏移量：上、下、左、右 int[] dx = {-1, 1, 0, 0}; int[] dy = {0, 0, -1, 1}; // BFS遍历连通区域 while (!queue.isEmpty()) { int[] cur = queue.poll(); // 取出队列中的当前点 int x = cur[0]; int y = cur[1]; count++; // 增加连通区域点计数 // 更新连通区域的边界 minX = Math.min(minX, x); maxX = Math.max(maxX, x); minY = Math.min(minY, y); maxY = Math.max(maxY, y); // 遍历四个方向 for (int k = 0; k \u0026lt; 4; k++) { int nextX = x + dx[k]; int nextY = y + dy[k]; if (nextX \u0026gt;= 0 \u0026amp;\u0026amp; nextX \u0026lt; n \u0026amp;\u0026amp; nextY \u0026gt;= 0 \u0026amp;\u0026amp; nextY \u0026lt; m \u0026amp;\u0026amp; grid[nextX][nextY] == \u0026#39;.\u0026#39; \u0026amp;\u0026amp; !visit[nextX][nextY]) { visit[nextX][nextY] = true; queue.add(new int[]{nextX, nextY}); } } } // 判断是否填满整个区域 // 当count等于整个区域的格子数量时，返回true，表示已填满 // 整个区域的格子数量计算公式为：(maxX-minX+1) * (maxY-minY+1) return count == (maxX - minX + 1) * (maxY - minY + 1); } ","date":"2026-02-27T21:15:34Z","permalink":"https://iamxurulin.github.io/p/bfsbishi81-%E5%89%AA%E7%BA%B8%E6%B8%B8%E6%88%8F/","title":"【BFS】BISHI81 剪纸游戏"},{"content":" 思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 /** * 主方法，处理输入输出并调用BFS算法 * * @param args 命令行参数 * @throws IOException 可能抛出IO异常 */ public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，使用PrintWriter输出 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取第一行输入，分割成字符串数组并解析为整数n和m String[] strA = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int n = Integer.parseInt(strA[0]); // 行数 int m = Integer.parseInt(strA[1]); // 列数 // 读取第二行输入，分割成字符串数组并解析为起点和终点的坐标 String[] strB = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int xs = Integer.parseInt(strB[0]) - 1; // 起点x坐标（转换为0-based索引） int ys = Integer.parseInt(strB[1]) - 1; // 起点y坐标（转换为0-based索引） int xt = Integer.parseInt(strB[2]) - 1; // 终点x坐标（转换为0-based索引） int yt = Integer.parseInt(strB[3]) - 1; // 终点y坐标（转换为0-based索引） // 读取n行输入，构建二维字符数组grid表示地图 char[][] grid = new char[n][m]; for (int i = 0; i \u0026lt; n; i++) { grid[i] = br.readLine().trim().toCharArray(); } // 调用BFS方法计算最短路径长度，并将结果输出 out.println(bfs(grid, n, m, xs, ys, xt, yt)); // 刷新输出流，确保所有内容都被写出 out.flush(); // 关闭输出流和输入流 out.close(); br.close(); } /** * 使用广度优先搜索(BFS)算法计算从起点到终点的最短路径 * * @param grid 二维字符数组，表示网格地图，\u0026#39;.\u0026#39;表示可走的路径 * @param n 网格的行数 * @param m 网格的列数 * @param xs 起点的x坐标 * @param ys 起点的y坐标 * @param xt 终点的x坐标 * @param yt 终点的y坐标 * @return 从起点到终点的最短距离，如果无法到达则返回-1 */ private static int bfs(char[][] grid, int n, int m, int xs, int ys, int xt, int yt) { // 如果起点和终点相同，直接返回0 if (xs == xt \u0026amp;\u0026amp; ys == yt) { return 0; } // 初始化距离数组，-1表示未访问过 int[][] distance = new int[n][m]; for (int i = 0; i \u0026lt; n; i++) { Arrays.fill(distance[i], -1); } // 使用队列实现BFS，存储待访问的节点坐标 Queue\u0026lt;int[]\u0026gt; queue = new LinkedList\u0026lt;\u0026gt;(); queue.add(new int[]{xs, ys}); distance[xs][ys] = 0; // 起点到自身的距离为0 // 定义四个方向的偏移量，分别表示上、下、左、右 int[] dx = {-1, 1, 0, 0}; int[] dy = {0, 0, -1, 1}; // BFS遍历 while (!queue.isEmpty()) { // 取出队列中的当前节点 int[] cur = queue.poll(); int x = cur[0]; int y = cur[1]; // 遍历四个方向 for (int i = 0; i \u0026lt; 4; i++) { // 计算下一个节点的坐标 int nextX = x + dx[i]; int nextY = y + dy[i]; // 检查下一个节点是否在网格范围内，且是可走的路径，且未被访问过 if (nextX \u0026gt;= 0 \u0026amp;\u0026amp; nextX \u0026lt; n \u0026amp;\u0026amp; nextY \u0026gt;= 0 \u0026amp;\u0026amp; nextY \u0026lt; m \u0026amp;\u0026amp; grid[nextX][nextY] == \u0026#39;.\u0026#39; \u0026amp;\u0026amp; distance[nextX][nextY] == -1) { // 更新距离 distance[nextX][nextY] = distance[x][y] + 1; // 如果到达终点，返回距离 if (nextX == xt \u0026amp;\u0026amp; nextY == yt) { return distance[nextX][nextY]; } // 将下一个节点加入队列 queue.add(new int[]{nextX, nextY}); } } } // 如果无法到达终点，返回-1 return -1; } ","date":"2026-02-27T20:18:34Z","permalink":"https://iamxurulin.github.io/p/bfs-%E6%96%B9%E5%90%91%E6%95%B0%E7%BB%84bishi80-%E8%B5%B0%E8%BF%B7%E5%AE%AB-%E6%96%B9%E5%90%91%E6%95%B0%E7%BB%84/","title":"【BFS-方向数组】BISHI80 走迷宫-方向数组"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 private static int N, M; // 网格的行数和列数 private static int[][] grid; // 存储网格数据的二维数组 private static boolean[][] used; // 标记网格位置是否被使用过的二维数组 private static long ans; // 存储最终答案的最大值 /** * 主方法，程序的入口点 * * @param args 命令行参数 * @throws IOException 可能抛出IO异常 */ public static void main(String[] args) throws IOException { // 使用BufferedReader从标准输入读取数据 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // 使用PrintWriter向标准输出写入数据 PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取测试用例数量 int T = Integer.parseInt(br.readLine().trim()); // 处理每个测试用例 for (int i = 0; i \u0026lt; T; i++) { String[] str = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); N = Integer.parseInt(str[0]); M = Integer.parseInt(str[1]); grid = new int[N][M]; // 初始化网格数组 used = new boolean[N][M]; // 初始化使用标记数组 ans = 0; // 重置答案为0 // 读取网格数据 for (int j = 0; j \u0026lt; N; j++) { String[] gridStr = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); for (int k = 0; k \u0026lt; M; k++) { grid[j][k] = Integer.parseInt(gridStr[k]); // 将字符串转换为整数存入网格 } } backtrack(0, 0); // 从第一个位置开始回溯 out.println(Math.max(ans, 0)); // 输出最大答案，至少为0 } out.flush(); // 刷新输出缓冲区 out.close(); // 关闭输出流 br.close(); // 关闭输入流 } /** * 回溯算法方法，用于在网格中寻找最大可能值 * * @param index 当前处理的位置索引，从0到N*M-1 * @param curAns 当前累计的答案值 */ private static void backtrack(int index, long curAns) { // 基本情况：如果已经处理完所有网格位置 if (index == N * M) { // 更新最大答案值 ans = Math.max(ans, curAns); return; } // 计算当前索引对应的行和列 int row = index / M; // 计算当前行号 int col = index % M; // 计算当前列号 // 情况1：不选择当前网格位置，直接处理下一个位置 backtrack(index + 1, curAns); // 情况2：如果当前位置可以安全选择 if (isSafe(row, col)) { // 标记当前位置为已使用 used[row][col] = true; // 选择当前位置，累加其值并处理下一个位置 backtrack(index + 1, curAns + grid[row][col]); // 回溯：撤销当前位置的使用标记 used[row][col] = false; } } /** * 检查指定位置是否安全，即其周围8个方向（包括自身）是否都没有被使用过 * * @param row 当前行索引 * @param col 当前列索引 * @return 如果周围8个方向都没有被使用过，返回true；否则返回false */ private static boolean isSafe(int row, int col) { // 遍历当前位置的周围8个方向（包括自身） for (int drow = -1; drow \u0026lt;= 1; drow++) { for (int dcol = -1; dcol \u0026lt;= 1; dcol++) { // 计算下一个位置的行索引 int nextRow = row + drow; // 计算下一个位置的列索引 int nextCol = col + dcol; // 检查下一个位置是否在有效范围内 if (nextRow \u0026gt;= 0 \u0026amp;\u0026amp; nextRow \u0026lt; N \u0026amp;\u0026amp; nextCol \u0026gt;= 0 \u0026amp;\u0026amp; nextCol \u0026lt; M) { // 如果下一个位置已经被使用过，则返回false if (used[nextRow][nextCol]) { return false; } } } } // 如果周围8个方向都没有被使用过，则返回true return true; } ","date":"2026-02-26T21:45:07Z","permalink":"https://iamxurulin.github.io/p/bishi79-%E5%8F%96%E6%95%B0%E6%B8%B8%E6%88%8F/","title":"BISHI79 取数游戏"},{"content":"\n思路 【回溯】没有重复项数字的全排列 这个是核心代码模式的题解。现在这题是ACM模式的题解。 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 /** * 主方法，程序的入口点 * * @param args 命令行参数 * @throws IOException 可能抛出IO异常 */ public static void main(String[] args) throws IOException { // 使用BufferedReader从标准输入读取数据 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // 使用PrintWriter向标准输出写入数据 PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取整数n，并去除可能的空白字符 int n = Integer.parseInt(br.readLine().trim()); // 初始化一个长度为n的数组，用于存储排列结果 int[] track = new int[n]; // 初始化一个长度为n+1的布尔数组，用于标记数字是否已被使用 boolean[] used = new boolean[n + 1]; // 调用回溯方法生成全排列 backtrack(n, 0, track, used, out); // 刷新输出流，确保所有数据都被写出 out.flush(); // 关闭输出流 out.close(); // 关闭输入流 br.close(); } /** * 回溯法生成全排列 * * @param n 排列的数字范围，1到n * @param depth 当前递归的深度，即已经确定的前几个数字的数量 * @param track 当前排列的路径，存储已经确定的数字序列 * @param used 标记数组，记录数字是否已经在当前排列中使用过 * @param out 用于输出结果的打印流 */ private static void backtrack(int n, int depth, int[] track, boolean[] used, PrintWriter out) { // 如果当前深度等于n，说明已经生成了一个完整的排列 if (depth == n) { // 输出当前排列 for (int i = 0; i \u0026lt; n; i++) { // 如果是最后一个数字，直接输出，否则输出数字加空格 out.print(track[i] + (i == n - 1 ? \u0026#34;\u0026#34; : \u0026#34; \u0026#34;)); } // 换行，准备输出下一个排列 out.println(); // 返回上一层递归 return; } // 遍历所有可能的数字 for (int i = 1; i \u0026lt;= n; i++) { // 如果数字i还没有被使用过 if (!used[i]) { // 标记数字i为已使用 used[i] = true; // 将数字i放入当前深度的位置 track[depth] = i; // 递归处理下一个位置 backtrack(n, depth + 1, track, used, out); // 回溯，撤销选择，将数字i标记为未使用，以便尝试其他可能性 used[i] = false; } } } ","date":"2026-02-26T20:32:41Z","permalink":"https://iamxurulin.github.io/p/bishi78-%E5%85%A8%E6%8E%92%E5%88%97/","title":"BISHI78 全排列"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 /** * 主方法，程序的入口点 * * @param args 命令行参数 * @throws IOException 可能抛出IO异常 */ public static void main(String[] args) throws IOException { // 使用BufferedReader读取标准输入，用于高效读取输入数据 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // 使用PrintWriter输出结果，提供高效的输出功能 PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取第一行输入，并按空白字符分割成字符串数组 String[] strA = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); // 解析第一行输入中的两个整数，n和m int n = Integer.parseInt(strA[0]); // 网格的行数 int m = Integer.parseInt(strA[1]); // 网格的列数 // 创建字符串数组用于存储网格数据 String[] grid = new String[n]; // 创建二维字符数组，用于存储网格的字符表示 char[][] gridStr = new char[n][m]; // 读取网格数据 for (int i = 0; i \u0026lt; n; i++) { grid[i] = br.readLine().trim(); // 读取一行并去除首尾空白 gridStr[i] = grid[i].toCharArray(); // 将字符串转换为字符数组 } int ans = 0; // 用于记录找到的\u0026#39;W\u0026#39;的数量 // 遍历网格中的每个元素 for (int i = 0; i \u0026lt; n; i++) { for (int j = 0; j \u0026lt; m; j++) { // 如果当前元素是\u0026#39;W\u0026#39;，则增加计数器并执行深度优先搜索 if (gridStr[i][j] == \u0026#39;W\u0026#39;) { ans++; // 增加计数器 dfs(gridStr, i, j); // 执行深度优先搜索，标记相邻的\u0026#39;W\u0026#39; } } } // 输出结果 out.println(ans); // 刷新输出流，确保所有数据都被写出 out.flush(); // 关闭输出流 out.close(); // 关闭输入流 br.close(); } /** * 深度优先搜索(DFS)方法，用于遍历网格中的连通区域 * * @param grid 二维字符网格 * @param i 当前处理的行坐标 * @param j 当前处理的列坐标 */ private static void dfs(char[][] grid, int i, int j) { // 检查当前坐标是否超出网格边界 if (i \u0026lt; 0 || j \u0026lt; 0 || i \u0026gt;= grid.length || j \u0026gt;= grid[0].length) { return; } // 如果当前格子是\u0026#39;.\u0026#39;，表示已经是访问过的格子，直接返回 if (grid[i][j] == \u0026#39;.\u0026#39;) { return; } // 将当前格子标记为已访问（用\u0026#39;.\u0026#39;表示） grid[i][j] = \u0026#39;.\u0026#39;; // 遍历当前格子的8个相邻方向（包括对角线方向） for (int di = -1; di \u0026lt;= 1; di++) { for (int dj = -1; dj \u0026lt;= 1; dj++) { // 跳过当前格子本身 if (di == 0 \u0026amp;\u0026amp; dj == 0) { continue; } // 递归处理相邻格子 dfs(grid, i + di, j + dj); } } } ","date":"2026-02-25T22:45:37Z","permalink":"https://iamxurulin.github.io/p/dfsbishi77%E6%95%B0%E6%B0%B4%E5%9D%91/","title":"【DFS】BISHI77数水坑"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 private static String ans = \u0026#34;No\u0026#34;; /** * 主方法，程序的入口点 * * @param args 命令行参数 * @throws IOException 可能抛出IO异常 */ public static void main(String[] args) throws IOException { // 使用BufferedReader读取标准输入，用于高效读取输入数据 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // 使用PrintWriter输出结果，提供高效的输出功能 PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取第一行输入，并按空白字符分割成字符串数组 String[] strA = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); // 解析第一行输入中的两个整数，n和m int n = Integer.parseInt(strA[0]); // 网格的行数 int m = Integer.parseInt(strA[1]); // 网格的列数 // 创建字符串数组用于存储网格数据 String[] grid = new String[n]; // 创建二维字符数组，用于存储网格的字符表示 char[][] gridStr = new char[n][m]; // 读取网格数据 for (int i = 0; i \u0026lt; n; i++) { grid[i] = br.readLine().trim(); // 读取一行并去除首尾空白 gridStr[i] = grid[i].toCharArray(); // 将字符串转换为字符数组 } // 从起点(0,0)开始深度优先搜索 dfs(gridStr, 0, 0); // 输出结果 out.println(ans); // 刷新输出流，确保所有数据都被写出 out.flush(); // 关闭输出流 out.close(); // 关闭输入流 br.close(); } /** * 深度优先搜索（DFS）方法，用于在二维网格中寻找从起点到终点的路径 * * @param grid 二维字符网格，包含可走的路径和障碍物 * @param i 当前搜索的行索引 * @param j 当前搜索的列索引 */ private static void dfs(char[][] grid, int i, int j) { // 检查当前坐标是否超出网格边界 if (i \u0026lt; 0 || j \u0026lt; 0 || i \u0026gt;= grid.length || j \u0026gt;= grid[0].length) { return; } // 检查当前单元格是否是障碍物（已访问过的标记为\u0026#39;#\u0026#39;） if (grid[i][j] == \u0026#39;#\u0026#39;) { return; } // 检查是否到达终点（右下角） if (i == grid.length - 1 \u0026amp;\u0026amp; j == grid[0].length - 1) { ans = \u0026#34;Yes\u0026#34;; // 找到路径，设置答案为\u0026#34;Yes\u0026#34; return; } // 标记当前单元格为已访问（使用\u0026#39;#\u0026#39;作为标记） grid[i][j] = \u0026#39;#\u0026#39;; // 向上搜索 dfs(grid, i - 1, j); // 向下搜索 dfs(grid, i + 1, j); // 向左搜索 dfs(grid, i, j - 1); // 向右搜索 dfs(grid, i, j + 1); } ","date":"2026-02-25T21:45:53Z","permalink":"https://iamxurulin.github.io/p/dfsbishi76-%E8%BF%B7%E5%AE%AB%E5%AF%BB%E8%B7%AF/","title":"【DFS】BISHI76 迷宫寻路"},{"content":"在做个人项目的时候，有时为了开源需要对数据进行脱敏，尤其是一些log文件。\n先确认代码里还有没有其他“XXX”？\n执行这条精准排除命令：\n1 git grep -w \u0026#34;XXX\u0026#34; $(git rev-list --all) -- \u0026#34;:!*.log\u0026#34; 如果没有输出，证明没有包含相关字符串的文件，如果有输出则需要进行如下操作，彻底删除文件的历史。\n⚠️警告，以下操作会改写所有 Commit 的 Hash 值，必须强制推送！\n1.在项目根目录执行清洗命令 1 2 3 git filter-branch --force --index-filter \\ \u0026#39;git rm --cached --ignore-unmatch \u0026#34;*/hs_err_pid*.log\u0026#34;\u0026#39; \\ --prune-empty --tag-name-filter cat -- --all 执行过程中终端会疯狂滚动 “Rewriting \u0026hellip;”，耐心等待即可。\n2. 清理 Git 内部备份 1 2 3 rm -rf .git/refs/original/ git reflog expire --expire=now --all git gc --prune=now 3. 防止再次犯错，在 .gitignore 中永久屏蔽 1 2 3 # JVM 崩溃日志 hs_err_pid*.log *.log 4.删除本地log文件，强制推送覆盖远程仓库 1 2 3 4 5 # 删除当前文件 git rm --force hs_err_pid*.log git add .gitignore git commit -m \u0026#34;chore: 删除误上传的JVM崩溃日志 + 屏蔽所有.log文件提交\u0026#34; git push origin --force --all 5.验证是否彻底干净 1 git grep -w \u0026#34;XXX\u0026#34; $(git rev-list --all) 如果没有任何输出，则大功告成。\n","date":"2026-02-25T19:45:42Z","permalink":"https://iamxurulin.github.io/p/%E5%BC%80%E6%BA%90%E5%BF%85%E5%A4%87git-%E4%BB%93%E5%BA%93%E6%95%8F%E6%84%9F%E6%97%A5%E5%BF%97%E6%96%87%E4%BB%B6%E6%B8%85%E7%90%86%E4%B8%8E%E8%84%B1%E6%95%8F%E6%95%99%E7%A8%8B/","title":"开源必备：Git 仓库敏感日志文件清理与脱敏教程"},{"content":"\n如果 $a^b \\ge m$，返回 $(a^b \\bmod m) + m$；否则直接返回 $a^b$。\n思路 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 private static long MOD = 1000000007; // 定义模数常量，用于取模运算 private static List\u0026lt;Long\u0026gt; mList = new ArrayList\u0026lt;\u0026gt;(); // 存储欧拉函数值的列表 /** * 主方法，处理输入输出并计算结果 * * @param args 命令行参数 * @throws IOException 可能抛出IO异常 */ public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，PrintWriter输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取输入的长整型数值n long n = Long.parseLong(br.readLine().trim()); // 初始化tmp为MOD值，并添加到mList列表中 long tmp = MOD; mList.add(tmp); // 计算欧拉函数值直到tmp为1，并将结果添加到mList中 while (tmp \u0026gt; 1) { tmp = getPhi(tmp); mList.add(tmp); } // 调用solve方法计算结果并输出，取模MOD out.println(solve(n, 0) % MOD); // 清空输出缓冲区并关闭资源 out.flush(); out.close(); br.close(); } /** * 递归求解模幂运算的结果 * * @param curN 当前处理的数字 * @param idx 当前处理的mList中的索引位置 * @return 返回模幂运算的结果 */ private static long solve(long curN, int idx) { // 获取当前索引对应的模数 long m = mList.get(idx); // 如果模数为1，任何数对1取模结果都是1 if (m == 1) { return 1; } // 如果当前数字为1，1的任何次幂都是1 if (curN == 1) { return 1; } // 递归计算指数部分，curN-1和idx+1表示递归处理下一个数字和下一个模数 long exp = solve(curN - 1, idx + 1); // 计算curN的exp次幂对m取模的结果 return safePow(curN, exp, m); } /** * 安全的幂运算函数，计算 a^b mod m，并处理可能的溢出情况 * * @param a 底数 * @param b 指数 * @param m 模数 * @return 计算结果，如果发生溢出则返回结果加上模数后的值 */ private static long safePow(long a, long b, long m) { long res = 1; // 初始化结果为1 boolean overflow = false; // 溢出标志，初始为false // 如果底数大于等于模数，进行取模操作，并标记可能溢出 if (a \u0026gt;= m) { overflow = true; a %= m; } // 使用快速幂算法计算幂运算 while (b \u0026gt; 0) { // 如果当前指数的二进制最低位为1（即指数为奇数） if ((b \u0026amp; 1) == 1) { res = res * a; // 将当前底数乘入结果 // 如果结果大于等于模数，进行取模操作，并标记可能溢出 if (res \u0026gt;= m) { overflow = true; res %= m; } } b \u0026gt;\u0026gt;= 1; // 指数右移一位，相当于除以2 // 如果指数还有剩余位 if (b \u0026gt; 0) { a = a * a; // 底数平方 // 如果平方后的底数大于等于模数，进行取模操作，并标记可能溢出 if (a \u0026gt;= m) { overflow = true; a %= m; } } } // 根据溢出标志返回结果，如果溢出则加上模数 return overflow ? (res + m) : res; } /** * 计算欧拉函数Phi(n)的值 * 欧拉函数Phi(n)表示小于n的正整数中与n互质的数的个数 * * @param n 需要计算欧拉函数的正整数 * @return 返回n的欧拉函数值 */ private static long getPhi(long n) { long res = n; // 初始化结果为n // 遍历2到sqrt(n)的所有整数 for (long i = 2; i * i \u0026lt;= n; i++) { // 如果i是n的因数 if (n % i == 0) { // 应用欧拉函数的公式：res = res * (1 - 1/p) res = res / i * (i - 1); // 去除n中所有的i因子 while (n % i == 0) { n /= i; } } } // 如果n大于1，说明n本身是一个质数 if (n \u0026gt; 1) { res = res / n * (n - 1); } return res; } ","date":"2026-02-24T21:40:06Z","permalink":"https://iamxurulin.github.io/p/bishi75-%E9%98%B6%E5%B9%82/","title":"BISHI75 阶幂"},{"content":"\n乘法逆元 $a^{-1} \\equiv x \\pmod m$ 等价于求解线性同余方程：\n$ax \\equiv 1 \\pmod m \\implies ax + my = 1$\n根据裴蜀定理，该方程有解的充要条件是 $\\gcd(a, m) = 1$。\n扩展欧几里得算法可以在计算 $\\gcd(a, m)$ 的同时，求出一组整数 $(x, y)$ 满足 $ax + my = \\gcd(a, m)$。\n思路 实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 private static long x; private static long y; public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，PrintWriter输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取测试用例数量 int T = Integer.parseInt(br.readLine().trim()); // 处理每个测试用例 for (int i = 0; i \u0026lt; T; i++) { String[] str = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); long a = Long.parseLong(str[0]); long m = Long.parseLong(str[1]); exgcd(a, m); long ans = (x % m + m) % m; out.println(ans); } // 清空输出缓冲区并关闭资源 out.flush(); out.close(); br.close(); } /** * 扩展欧几里得算法，用于求解形如 ax + by = gcd(a,b) 的方程 * 同时返回最大公约数和x、y的值 * * @param a 第一个参数 * @param b 第二个参数 * @return a和b的最大公约数 */ private static long exgcd(long a, long b) { // 当b为0时，递归结束，此时a就是最大公约数 if (b == 0) { x = 1; // x初始化为1 y = 0; // y初始化为0 return a; } // 递归调用，计算b和a mod b的最大公约数 long d = exgcd(b, a % b); // 通过递归结果更新x和y的值 long tmp = x; x = y; y = tmp - (a / b) * y; // 返回最大公约数 return d; } ","date":"2026-02-24T20:34:31Z","permalink":"https://iamxurulin.github.io/p/bishi74-%E6%A8%A1%E6%9D%BF%E9%9D%9E%E8%B4%A8%E6%A8%A1%E6%95%B0%E4%B8%8B%E7%9A%84%E4%B9%98%E6%B3%95%E9%80%86%E5%85%83/","title":"BISHI74 【模板】非质模数下的乘法逆元"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，PrintWriter输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取测试用例数量 int T = Integer.parseInt(br.readLine().trim()); // 处理每个测试用例 for (int i = 0; i \u0026lt; T; i++) { long x = Long.parseLong(br.readLine().trim()); out.println(euler(x)); // 输出当前测试用例的结果 } // 清空输出缓冲区并关闭资源 out.flush(); out.close(); br.close(); } /** * 使用欧拉函数计算小于等于x的正整数中与x互质的数的个数 * 欧拉函数公式：φ(n) = n * (1 - 1/p1) * (1 - 1/p2) * ... * (1 - 1/pk) * 其中p1, p2, ..., pk是n的质因数 * * @param x 需要计算欧拉函数的正整数 * @return 小于等于x的正整数中与x互质的数的个数 */ private static long euler(long x) { // 如果x为1，直接返回1，因为1与1互质 if (x == 1) { return 1; } // 初始化结果为x long res = x; // 遍历从2到√x的所有数，寻找x的质因数 for (long i = 2; i * i \u0026lt;= x; i++) { // 如果i是x的因数 if (x % i == 0) { // 应用欧拉函数公式，乘以(1-1/i) res = res / i * (i - 1); // 去除x中所有的i因子 while (x % i == 0) { x /= i; } } } // 如果x大于1，说明x本身是一个质数，应用欧拉函数公式 if (x \u0026gt; 1) { res = res / x * (x - 1); } return res; } ","date":"2026-02-23T21:59:20Z","permalink":"https://iamxurulin.github.io/p/bishi73-%E6%A8%A1%E6%9D%BF%E6%AC%A7%E6%8B%89%E5%87%BD%E6%95%B0%E8%AE%A1%E7%AE%97-%E6%9C%B4%E7%B4%A0%E6%B1%82%E5%80%BC%E8%AF%95%E9%99%A4%E6%B3%95/","title":"BISHI73 【模板】欧拉函数计算Ⅰ ‖ 朴素求值：试除法"},{"content":"\n由于数组 $a$ 仅包含 $0$ 和 $1$，对于长度为奇数 $k$ 的子序列，其中位数是 $1$ 的充要条件是：子序列中 $1$ 的个数不少于 $\\frac{k+1}{2}$。\n因此，求“中位数之和”等价于求“中位数为 $1$的子序列个数”。\n思路 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 // 定义模数常量，用于所有取模运算 private static final int MOD = 1000000007; // 定义最大计算范围常量 private static final int MAX_N = 1000001; // 存储阶乘值的数组 private static long[] fact = new long[MAX_N]; // 存储阶乘逆元的数组 private static long[] invFact = new long[MAX_N]; /** * 主方法，处理输入输出并计算组合数学问题 * * @param args 命令行参数 * @throws IOException 可能抛出的IO异常 */ public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，PrintWriter输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 预计算阶乘和逆元，为后续计算做准备 precompute(); // 读取测试用例数量 int T = Integer.parseInt(br.readLine().trim()); // 处理每个测试用例 for (int i = 0; i \u0026lt; T; i++) { // 读取每行输入并分割，获取n和k的值 String[] str = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); int n = Integer.parseInt(str[0]); // 总数 int k = Integer.parseInt(str[1]); // 选择数 // 统计输入中\u0026#34;1\u0026#34;的数量 int c1 = 0; String[] strA = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); for (int j = 0; j \u0026lt; n; j++) { if (strA[j].equals(\u0026#34;1\u0026#34;)) { c1++; // 统计\u0026#34;1\u0026#34;的个数 } } int c0 = n - c1; // 计算\u0026#34;0\u0026#34;的个数 // 计算至少需要选择的\u0026#34;1\u0026#34;的数量 int m = (k + 1) / 2; long ans = 0; // 遍历所有可能的\u0026#34;1\u0026#34;的选择数量 for (int j = m; j \u0026lt;= Math.min(k, c1); j++) { // 检查剩余的选择数是否不超过\u0026#34;0\u0026#34;的数量 if (k - j \u0026lt;= c0) { // 计算组合数并累加结果 long res = (nCr(c1, j) * nCr(c0, k - j)) % MOD; ans = (ans + res) % MOD; } } out.println(ans); // 输出当前测试用例的结果 } // 清空输出缓冲区并关闭资源 out.flush(); out.close(); br.close(); } /** * 使用快速幂算法计算base的exp次方对MOD取模的结果 * 快速幂算法是一种高效计算幂运算的算法，时间复杂度为O(log n) * * @param base 底数 * @param exp 指数 * @return base的exp次方对MOD取模的结果 */ private static long mypower(long base, long exp) { // 初始化结果为1对MOD取模，防止MOD为1时结果错误 long ans = 1 % MOD; // 当指数exp大于0时继续循环 while (exp \u0026gt; 0) { // 如果当前指数是奇数，将base乘入结果 if (exp % 2 == 1) { ans = (ans * base) % MOD; } // base平方，指数减半 base = (base * base) % MOD; exp /= 2; } // 返回最终结果 return ans; } /** * 预计算阶乘和阶乘的逆元数组 * 使用动态规划方法计算阶乘数组fact[] * 使用费马小定理计算逆元数组invFact[] */ private static void precompute() { // 初始化0的阶乘和0的阶乘逆元为1 fact[0] = 1; invFact[0] = 1; // 动态规划计算阶乘数组 for (int i = 1; i \u0026lt; MAX_N; i++) { fact[i] = (fact[i - 1] * i) % MOD; } // 使用费马小定理计算最大阶乘的逆元 invFact[MAX_N - 1] = mypower(fact[MAX_N - 1], MOD - 2); // 逆向递推计算其他阶乘的逆元 for (int i = MAX_N - 2; i \u0026gt;= 1; i--) { invFact[i] = (invFact[i + 1] * (i + 1)) % MOD; } } /** * 计算组合数 nCr，即从n个物品中选取r个物品的组合数 * 使用预计算阶乘和阶乘逆元的方法来高效计算组合数 * * @param n 总物品数 * @param r 选取物品数 * @return 组合数结果，对MOD取模 */ private static long nCr(int n, int r) { // 如果r小于0或大于n，组合数为0 if (r \u0026lt; 0 || r \u0026gt; n) { return 0; } // 使用公式 nCr = n! / (r! * (n-r)!) 计算 // 通过预计算的阶乘数组fact和阶乘逆元数组invFact来计算 // 并对MOD取模，防止数值溢出 return (((fact[n] * invFact[r]) % MOD) * invFact[n - r]) % MOD; } ","date":"2026-02-23T21:10:27Z","permalink":"https://iamxurulin.github.io/p/bishi72-%E4%B8%AD%E4%BD%8D%E6%95%B0%E4%B9%8B%E5%92%8C/","title":"BISHI72 中位数之和"},{"content":"\n思路 这题就是个组合问题，本质上是求C(n,5)+C(n,6)+C(n,7)的问题。\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 // 定义模数常量，用于所有取模运算 private static final int MOD = 1000000007; // 定义最大计算范围常量 private static final int MAX_N = 1000001; // 存储阶乘值的数组 private static long[] fact = new long[MAX_N]; // 存储阶乘逆元的数组 private static long[] invFact = new long[MAX_N]; public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，PrintWriter输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 预计算阶乘和逆元 precompute(); int n = Integer.parseInt(br.readLine().trim()); long ans = (nCr(n, 5) + nCr(n, 6) + nCr(n, 7))%MOD; out.println(ans); // 清空输出缓冲区并关闭资源 out.flush(); out.close(); br.close(); } /** * 使用快速幂算法计算base的exp次方对MOD取模的结果 * 快速幂算法是一种高效计算幂运算的算法，时间复杂度为O(log n) * * @param base 底数 * @param exp 指数 * @return base的exp次方对MOD取模的结果 */ private static long mypower(long base, long exp) { // 初始化结果为1对MOD取模，防止MOD为1时结果错误 long ans = 1 % MOD; // 当指数exp大于0时继续循环 while (exp \u0026gt; 0) { // 如果当前指数是奇数，将base乘入结果 if (exp % 2 == 1) { ans = (ans * base) % MOD; } // base平方，指数减半 base = (base * base) % MOD; exp /= 2; } // 返回最终结果 return ans; } /** * 预计算阶乘和阶乘的逆元数组 * 使用动态规划方法计算阶乘数组fact[] * 使用费马小定理计算逆元数组invFact[] */ private static void precompute() { // 初始化0的阶乘和0的阶乘逆元为1 fact[0] = 1; invFact[0] = 1; // 动态规划计算阶乘数组 for (int i = 1; i \u0026lt; MAX_N; i++) { fact[i] = (fact[i - 1] * i) % MOD; } // 使用费马小定理计算最大阶乘的逆元 invFact[MAX_N - 1] = mypower(fact[MAX_N - 1], MOD - 2); // 逆向递推计算其他阶乘的逆元 for (int i = MAX_N - 2; i \u0026gt;= 1; i--) { invFact[i] = (invFact[i + 1] * (i + 1)) % MOD; } } /** * 计算组合数 nCr，即从n个物品中选取r个物品的组合数 * 使用预计算阶乘和阶乘逆元的方法来高效计算组合数 * * @param n 总物品数 * @param r 选取物品数 * @return 组合数结果，对MOD取模 */ private static long nCr(int n, int r) { // 如果r小于0或大于n，组合数为0 if (r \u0026lt; 0 || r \u0026gt; n) { return 0; } // 使用公式 nCr = n! / (r! * (n-r)!) 计算 // 通过预计算的阶乘数组fact和阶乘逆元数组invFact来计算 // 并对MOD取模，防止数值溢出 return (((fact[n] * invFact[r]) % MOD) * invFact[n - r]) % MOD; } ","date":"2026-02-22T23:25:15Z","permalink":"https://iamxurulin.github.io/p/bishi71-%E4%BA%BA%E5%91%98%E5%88%86%E7%BB%84%E9%97%AE%E9%A2%98/","title":"BISHI71 人员分组问题"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 // 定义模数常量，用于所有取模运算 private static final int MOD = 1000000007; // 定义最大计算范围常量 private static final int MAX_N = 500001; // 存储阶乘值的数组 private static long[] fact = new long[MAX_N]; // 存储阶乘逆元的数组 private static long[] invFact = new long[MAX_N]; /** * 主方法 * 处理输入输出，调用预计算和组合数计算方法 * @param args 命令行参数 * @throws IOException 可能抛出的IO异常 */ public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，PrintWriter输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 预计算阶乘和逆元 precompute(); // 读取测试用例数量 int T = Integer.parseInt(br.readLine().trim()); // 处理每个测试用例 for (int i = 0; i \u0026lt; T; i++) { // 读取每行输入并分割 String[] str = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); int n = Integer.parseInt(str[0]); int m = Integer.parseInt(str[1]); // 输出组合数计算结果 out.println(nCr(m, n)); } // 清空输出缓冲区并关闭资源 out.flush(); out.close(); br.close(); } /** * 使用快速幂算法计算base的exp次方对MOD取模的结果 * 快速幂算法是一种高效计算幂运算的算法，时间复杂度为O(log n) * * @param base 底数 * @param exp 指数 * @return base的exp次方对MOD取模的结果 */ private static long mypower(long base, long exp) { // 初始化结果为1对MOD取模，防止MOD为1时结果错误 long ans = 1 % MOD; // 当指数exp大于0时继续循环 while (exp \u0026gt; 0) { // 如果当前指数是奇数，将base乘入结果 if (exp % 2 == 1) { ans = (ans * base) % MOD; } // base平方，指数减半 base = (base * base) % MOD; exp /= 2; } // 返回最终结果 return ans; } /** * 预计算阶乘和阶乘的逆元数组 * 使用动态规划方法计算阶乘数组fact[] * 使用费马小定理计算逆元数组invFact[] */ private static void precompute() { // 初始化0的阶乘和0的阶乘逆元为1 fact[0] = 1; invFact[0] = 1; // 动态规划计算阶乘数组 for (int i = 1; i \u0026lt; MAX_N; i++) { fact[i] = (fact[i - 1] * i) % MOD; } // 使用费马小定理计算最大阶乘的逆元 invFact[MAX_N - 1] = mypower(fact[MAX_N - 1], MOD - 2); // 逆向递推计算其他阶乘的逆元 for (int i = MAX_N - 2; i \u0026gt;= 1; i--) { invFact[i] = (invFact[i + 1] * (i + 1)) % MOD; } } /** * 计算组合数 nCr，即从n个物品中选取r个物品的组合数 * 使用预计算阶乘和阶乘逆元的方法来高效计算组合数 * * @param n 总物品数 * @param r 选取物品数 * @return 组合数结果，对MOD取模 */ private static long nCr(int n, int r) { // 如果r小于0或大于n，组合数为0 if (r \u0026lt; 0 || r \u0026gt; n) { return 0; } // 使用公式 nCr = n! / (r! * (n-r)!) 计算 // 通过预计算的阶乘数组fact和阶乘逆元数组invFact来计算 // 并对MOD取模，防止数值溢出 return (((fact[n] * invFact[r]) % MOD) * invFact[n - r]) % MOD; } ","date":"2026-02-22T21:40:22Z","permalink":"https://iamxurulin.github.io/p/bishi70-%E6%A8%A1%E6%9D%BF%E7%BB%84%E5%90%88%E6%95%B0/","title":"BISHI70 【模板】组合数"},{"content":" 这个问题可以通过计算“总方案数”减去“不越狱方案数”来得出结果。\n总分配方案数： 每个房间有 $M$ 种宗教选择，共有 $N$ 个房间。 总数 = $M \\times M \\times \\dots \\times M = M^N$。\n不越狱方案数: 第 1 个房间有 $M$ 种选择； 第 2 个房间为了不与第 1 个重复，有 $M-1$ 种选择； 第 3 个房间为了不与第 2 个重复，有 $M-1$ 种选择； 以此类推，剩下的 $N-1$ 个房间每个都有 $M-1$ 种选择。 不越狱总数 = $M \\times (M-1)^{N-1}$。\n可能发生越狱的方案数： 越狱方案 = 总方案数 - 不越狱方案数 结果 = $M^N - M \\times (M-1)^{N-1}$\n流程图 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 private static final long MOD = 100003L; public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，PrintWriter输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); String[] str = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); long M = Long.parseLong(str[0]); long N = Long.parseLong(str[1]); long total = mypower(M, N, MOD); long safe = M * mypower(M - 1, N - 1, MOD)%MOD; long ans = (total-safe+MOD)%MOD; out.println(ans); out.flush(); out.close(); br.close(); } private static long mypower(long base, long exp, long mod) { long ans = 1 % mod; while (exp \u0026gt; 0) { if (exp % 2 == 1) { ans = (ans * base) % mod; } base = (base * base) % mod; exp /= 2; } return ans; } ","date":"2026-02-22T00:00:54Z","permalink":"https://iamxurulin.github.io/p/bishi69-hnoi2008%E8%B6%8A%E7%8B%B1/","title":"BISHI69 [HNOI2008]越狱"},{"content":" 这是一个经典的容斥原理应用题。\n设三个集合分别为 $A$（新手）、$B$（算法入门）、$C$（算法进阶）。\n已知 $n = |A \\cup B \\cup C|$ （至少刷过一个的总人数） $a = |A|, b = |B|, c = |C|$ （各题单人数） $d$: 恰好刷过两个题单的人数。 设 $x$: 同时刷过三个题单的人数（我们要找的解）。\n根据容斥原理：\n$$|A \\cup B \\cup C| = |A| + |B| + |C| - (|A \\cap B| + |A \\cap C| + |B \\cap C|) + |A \\cap B \\cap C|$$其中，任意两个集合的交集（如 $|A \\cap B|$）包含了“恰好刷过两个”的人和“刷过全部三个”的人。 因此：$|A \\cap B| + |A \\cap C| + |B \\cap C| = d + 3x$（因为 $x$ 这部分人在三个两两交集中各被算了一次）。\n代入公式： $$n = a + b + c - (d + 3x) + x$$ $$n = a + b + c - d - 2x$$得出 $x$： $$2x = a + b + c - n - d$$ $$x = \\frac{a + b + c - n - d}{2}$$ 流程图 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，PrintWriter输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int T = Integer.parseInt(br.readLine().trim()); for (int i = 0; i \u0026lt; T; i++) { String[] str = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); long n = Long.parseLong(str[0]); long a = Long.parseLong(str[1]); long b = Long.parseLong(str[2]); long c = Long.parseLong(str[3]); long d = Long.parseLong(str[4]); long ans = (a+b+c-n-d)/2; out.println(ans); } out.flush(); out.close(); br.close(); } ","date":"2026-02-21T23:32:54Z","permalink":"https://iamxurulin.github.io/p/bishi68-%E5%88%B7%E9%A2%98%E7%BB%9F%E8%AE%A1/","title":"BISHI68 刷题统计"},{"content":"\n思路 ###求解代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，PrintWriter输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int T = Integer.parseInt(br.readLine().trim()); for (int i = 0; i \u0026lt; T; i++) { String[] str = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); long a = Long.parseLong(str[0]); long b = Long.parseLong(str[1]); long c = Long.parseLong(str[2]); long ans = a*b+a*c+b*c; out.println(ans); } out.flush(); out.close(); br.close(); } ","date":"2026-02-21T23:03:31Z","permalink":"https://iamxurulin.github.io/p/bishi67-%E7%A9%BF%E6%90%AD%E5%A4%A7%E6%8C%91%E6%88%98/","title":"BISHI67 穿搭大挑战"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 private static final int MOD = 1000000007; /** * 主函数，处理输入输出并计算区间乘积 * * @param args 命令行参数 * @throws IOException 可能抛出IO异常 */ public static void main(String[] args) throws IOException { // 使用BufferedReader读取输入，PrintWriter输出结果 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取第一行输入，分割字符串并解析为整数n和q String[] str = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); int n = Integer.parseInt(str[0]); // 数组长度 int q = Integer.parseInt(str[1]); // 查询次数 // 创建前缀积数组，大小为n+1 long[] preProduct = new long[n + 1]; preProduct[0] = 1; // 初始化前缀积的第一个元素为1 // 读取数组元素并计算前缀积 String[] numStr = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); for (int i = 1; i \u0026lt;= n; i++) { long val = Long.parseLong(numStr[i - 1]); // 解析当前元素 // 计算前缀积，并对MOD取模 preProduct[i] = (preProduct[i - 1] * val) % MOD; } // 创建结果列表 List\u0026lt;Long\u0026gt; res = new ArrayList\u0026lt;\u0026gt;(); // 处理每个查询 for (int i = 0; i \u0026lt; q; i++) { // 读取查询区间 String[] strQ = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); int l = Integer.parseInt(strQ[0]); // 区间左端点 int r = Integer.parseInt(strQ[1]); // 区间右端点 // 计算preProduct[l-1]的模逆元 long invPreProduct = mypower(preProduct[l - 1], MOD - 2, MOD); // 计算区间[l,r]的乘积 long ans = (preProduct[r] * invPreProduct) % MOD; res.add(ans); // 将结果添加到列表中 } // 使用StringBuilder构建输出字符串 StringBuilder sb = new StringBuilder(); for (int i = 0; i \u0026lt; res.size(); i++) { // 每个结果用空格分隔，最后一个结果后不加空格 sb.append(res.get(i)).append(res.size() == n - 1 ? \u0026#34;\u0026#34; : \u0026#34; \u0026#34;); } // 输出结果并关闭流 out.println(sb.toString()); out.flush(); out.close(); br.close(); } private static long mypower(long base, long exp, long mod) { long ans = 1 % mod; while (exp \u0026gt; 0) { if (exp % 2 == 1) { ans = (ans * base) % mod; } base = (base * base) % mod; exp /= 2; } return ans; } ","date":"2026-02-21T22:12:44Z","permalink":"https://iamxurulin.github.io/p/bishi66-%E5%AD%90%E6%95%B0%E5%88%97%E6%B1%82%E7%A7%AF/","title":"BISHI66 子数列求积"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 private static final int MOD = 1000000007; public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int t = Integer.parseInt(br.readLine().trim()); for (int i = 0; i \u0026lt; t; i++) { String[] str = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); long a = Long.parseLong(str[0]); long b = Long.parseLong(str[1]); long bInv= mypower(b, MOD-2, MOD); long ans = (a%MOD+MOD)%MOD; ans = (ans*bInv)%MOD; out.println(ans); } out.flush(); out.close(); br.close(); } private static long mypower(long base, long exp, long mod) { long ans = 1 % mod; while (exp \u0026gt; 0) { if (exp % 2 == 1) { ans = (ans * base) % mod; } base = (base * base) % mod; exp /= 2; } return ans; } ","date":"2026-02-21T00:37:41Z","permalink":"https://iamxurulin.github.io/p/bishi65-%E6%A8%A1%E6%9D%BF%E5%88%86%E6%95%B0%E5%8F%96%E6%A8%A1/","title":"BISHI65 【模板】分数取模"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int T = Integer.parseInt(br.readLine().trim()); for (int i = 0; i \u0026lt; T; i++) { String[] str = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); long a = Long.parseLong(str[0]); long b = Long.parseLong(str[1]); long m = Long.parseLong(str[2]); out.println(mypower(a, b, m)); } out.flush(); out.close(); br.close(); } private static long mypower(long base, long exp, long mod) { long ans = 1 % mod; while (exp \u0026gt; 0) { if (exp % 2 == 1) { ans = (ans * base) % mod; } base = (base * base) % mod; exp /= 2; } return ans; } ","date":"2026-02-20T22:22:19Z","permalink":"https://iamxurulin.github.io/p/bishi64-%E6%A8%A1%E6%9D%BF%E5%BF%AB%E9%80%9F%E5%B9%82-%E6%A8%A1%E5%B0%8F%E6%95%B4%E6%95%B0/","title":"BISHI64 【模板】快速幂Ⅰ ‖ 模小整数"},{"content":"为了不占用 C 盘空间，重装系统也不丢失配置，本文详细记录了如何将 OpenClaw 安装在非系统盘，比如D 盘，并对接阿里云 通义千问大模型的全过程。\n主要是利用 NVM 和 npm 进行安装，方便后续一键升级，配置具有免费额度的Qwen API，这个只需要在阿里云百炼注册一个账号就能获得免费额度。\n准备工作 1.在安装之前，在D盘创建以下目录：\n1 2 D:\\SOFT_WARE\\OpenClaw\\data：用于存放配置文件、日志和数据库。 D:\\SOFT_WARE\\OpenClaw\\workspace：用于存放 AI 生成的代码和项目。 2.配置环境变量\n新建系统环境变量：OPENCLAW_HOME = D:\\SOFT_WARE\\OpenClaw\\data\n这个操作的目的是将原本在 C:\\Users\\用户名.openclaw 的配置转移到 D 盘。\n3.安装 NVM和npm\n下载地址：https://github.com/coreybutler/nvm-windows/releases\n安装后配置 NVM 路径：\n1 nvm root D:\\SOFT_WARE\\NVM 安装满足要求的 Node 版本：\n1 2 3 4 nvm install 22.12.0 nvm use 22.12.0 # 切换到该版本 node -v # 验证 Node 版本（显示 v22.12.0 则表示成功） npm -v # 验证 npm 可用 安装步骤 以管理员权限打开 PowerShell，执行：\n1 npm install -g openclaw 验证安装的版本：\n1 openclaw -v 配置 Qwen 大模型 以管理员权限打开 PowerShell，执行：\n1 [Environment]::SetEnvironmentVariable(\u0026#34;DASHSCOPE_API_KEY\u0026#34;, \u0026#34;你的sk-xxx密钥\u0026#34;, \u0026#34;User\u0026#34;) 修改 D:\\SOFT_WARE\\OpenClaw\\data.openclaw\\openclaw.json 文件。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 { \u0026#34;meta\u0026#34;: { \u0026#34;lastTouchedVersion\u0026#34;: \u0026#34;2026.2.19-2\u0026#34;, \u0026#34;lastTouchedAt\u0026#34;: \u0026#34;2026-02-20T04:31:04.294Z\u0026#34; }, \u0026#34;wizard\u0026#34;: { \u0026#34;lastRunAt\u0026#34;: \u0026#34;2026-02-20T03:22:24.306Z\u0026#34;, \u0026#34;lastRunVersion\u0026#34;: \u0026#34;2026.2.19-2\u0026#34;, \u0026#34;lastRunCommand\u0026#34;: \u0026#34;configure\u0026#34;, \u0026#34;lastRunMode\u0026#34;: \u0026#34;local\u0026#34; }, \u0026#34;agents\u0026#34;: { \u0026#34;defaults\u0026#34;: { \u0026#34;model\u0026#34;: { \u0026#34;primary\u0026#34;: \u0026#34;bailian/qwen3.5-plus-2026-02-15\u0026#34; }, \u0026#34;models\u0026#34;: { \u0026#34;bailian/qwen3.5-plus-2026-02-15\u0026#34;: { \u0026#34;alias\u0026#34;: \u0026#34;通义千问3.5 Plus\u0026#34; }, \u0026#34;bailian/qwen3.5-plus\u0026#34;: { \u0026#34;alias\u0026#34;: \u0026#34;通义千问 Plus\u0026#34; } }, \u0026#34;workspace\u0026#34;: \u0026#34;D:\\\\SOFT_WARE\\\\OpenClaw\\\\workspace\u0026#34;, \u0026#34;compaction\u0026#34;: { \u0026#34;mode\u0026#34;: \u0026#34;safeguard\u0026#34; }, \u0026#34;maxConcurrent\u0026#34;: 4, \u0026#34;subagents\u0026#34;: { \u0026#34;maxConcurrent\u0026#34;: 8 } } }, \u0026#34;models\u0026#34;: { \u0026#34;mode\u0026#34;: \u0026#34;merge\u0026#34;, \u0026#34;providers\u0026#34;: { \u0026#34;bailian\u0026#34;: { \u0026#34;baseUrl\u0026#34;: \u0026#34;https://dashscope.aliyuncs.com/compatible-mode/v1\u0026#34;, \u0026#34;apiKey\u0026#34;: \u0026#34;${DASHSCOPE_API_KEY}\u0026#34;, \u0026#34;api\u0026#34;: \u0026#34;openai-completions\u0026#34;, \u0026#34;models\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;qwen3.5-plus-2026-02-15\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;通义千问3.5 Plus\u0026#34;, \u0026#34;reasoning\u0026#34;: false, \u0026#34;input\u0026#34;: [\u0026#34;text\u0026#34;], \u0026#34;cost\u0026#34;: { \u0026#34;input\u0026#34;: 0.0025, \u0026#34;output\u0026#34;: 0.01 }, \u0026#34;contextWindow\u0026#34;: 262144, \u0026#34;maxTokens\u0026#34;: 65536 }, { \u0026#34;id\u0026#34;: \u0026#34;qwen3.5-plus\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;通义千问3.5 Plus\u0026#34;, \u0026#34;reasoning\u0026#34;: false, \u0026#34;input\u0026#34;: [\u0026#34;text\u0026#34;], \u0026#34;cost\u0026#34;: { \u0026#34;input\u0026#34;: 0.008, \u0026#34;output\u0026#34;: 0.008 }, \u0026#34;contextWindow\u0026#34;: 1029000, \u0026#34;maxTokens\u0026#34;: 32000 } ] } } }, \u0026#34;tools\u0026#34;: { \u0026#34;web\u0026#34;: { \u0026#34;search\u0026#34;: { \u0026#34;enabled\u0026#34;: true }, \u0026#34;fetch\u0026#34;: { \u0026#34;enabled\u0026#34;: true } } }, \u0026#34;gateway\u0026#34;: { \u0026#34;port\u0026#34;: 12345, \u0026#34;mode\u0026#34;: \u0026#34;local\u0026#34;, \u0026#34;bind\u0026#34;: \u0026#34;loopback\u0026#34;, \u0026#34;auth\u0026#34;: { \u0026#34;mode\u0026#34;: \u0026#34;token\u0026#34;, \u0026#34;token\u0026#34;: \u0026#34;你的自定义Token\u0026#34; } }, \u0026#34;commands\u0026#34;: { \u0026#34;native\u0026#34;: \u0026#34;auto\u0026#34;, \u0026#34;nativeSkills\u0026#34;: \u0026#34;auto\u0026#34;, \u0026#34;restart\u0026#34;: true } } 启动网关服务 OpenClaw 依靠网关（Gateway）提供网页交互界面。以管理员权限打开 PowerShell，执行：\n1 2 openclaw gateway install --force # 加 --force 避免残留配置冲突 openclaw gateway start 网页端使用与登录 在命令行输入\n1 openclaw config get gateway.auth.token 获取登录 Token或者直接从 JSON 中复制。\n打开浏览器访问 http://127.0.0.1:你的端口号\n小贴士 1.Windows 路径在 JSON 中必须使用双反斜杠 \\。\n2.浏览器的无痕模式会丢失本地缓存，可能会导致每次刷新页面都要重新输入 Token。\nhttp://127.0.0.1:你的端口号/?token=你的TOKEN\n修改 openclaw.json 后需重启网关（openclaw gateway restart）才能生效；\n升级 OpenClaw：npm update -g openclaw;\n5.卸载网关：openclaw gateway uninstall --force。\n6.\u0026quot;port\u0026quot;: 你的端口号 ➡️ 填写纯数字（如 12345）；\n7.\u0026quot;token\u0026quot;: \u0026quot;你的自定义Token\u0026quot; ➡️ 填写任意字符串（如 openclaw_123456）\nAI 浪潮下，工具的配置是第一步。希望这篇笔记能帮大家在 Windows 平台上少走弯路！\n","date":"2026-02-20T16:53:53Z","permalink":"https://iamxurulin.github.io/p/windows-%E7%8E%AF%E5%A2%83%E4%B8%8B-openclaw-%E7%9A%84%E5%AE%89%E8%A3%85%E4%B8%8E%E5%8D%83%E9%97%AE%E5%A4%A7%E6%A8%A1%E5%9E%8B%E9%85%8D%E7%BD%AE/","title":"Windows 环境下 OpenClaw 的安装与千问大模型配置"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private static final int MOD = 1000000007; public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int T = Integer.parseInt(br.readLine().trim()); while (T-- \u0026gt; 0) { int n = Integer.parseInt(br.readLine().trim()); long ans = 1; for(int i=1;i\u0026lt;=n;i++){ ans = (ans*i)%MOD; } out.println(ans%MOD); } out.flush(); out.close(); br.close(); } ","date":"2026-02-19T22:24:36Z","permalink":"https://iamxurulin.github.io/p/bishi63-%E8%AE%A1%E7%AE%97%E9%98%B6%E4%B9%98/","title":"BISHI63 计算阶乘"},{"content":" 思路 这道题本质上是要计算一个数字在二进制下“1”的个数，然后构造出另一个具有相同数量“1”但取值最小的数字。 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int T = Integer.parseInt(br.readLine().trim()); while (T-- \u0026gt; 0) { String str = br.readLine(); long n = Long.parseLong(str.trim()); int c = Long.bitCount(n); long k = (1L \u0026lt;\u0026lt; c) - 1; out.println(c + \u0026#34; \u0026#34; + k); } out.flush(); out.close(); br.close(); } ","date":"2026-02-19T21:23:17Z","permalink":"https://iamxurulin.github.io/p/bishi61-%E5%B0%8Fq%E7%9A%84%E6%95%B0%E5%88%97/","title":"BISHI61 小q的数列"},{"content":"测试环境：Windows 11 专业版 + Git 2.45.1\n下载Hugo 点击进入以下网页：https://github.com/gohugoio/hugo/tags\n可按需选择版本，如v0.154.5。\n也可以通过百度网盘链接下载：\n【hugo_extended_0.154.5_windows-amd64.zip】\n链接: https://pan.baidu.com/s/1XD1Zi877nFvVZaXw0Q9-wA 提取码: 477k\n创建个人博客 解压这个压缩包，进入到解压后的文件夹，右键进入命令行界面，输入以下命令【hugo new site CodeEdge】，其中【CodeEdge】名字可任意取。\n将【hugo.exe】文件复制到【CodeEdge】文件夹中，再cd到CodeEdge目录，执行以下命令启动服务【hugo server -D】\n配置个人博客的主题 点击进入以下网页https://github.com/CaiJimmy/hugo-theme-stack/tags，这里选择的是【Stack】主题，可按需选择版本，如v3.33.0。\n也可以通过百度网盘链接下载：\n【hugo-theme-stack-3.33.0.zip】\n链接: https://pan.baidu.com/s/1rNPzDXJbeFSrqURK4X1JJg 提取码: pwgv\n将这个压缩包解压到CodeEdge的themes文件夹下：\n将【exampleSite】文件夹下的【content】文件夹和【hugo.yaml】文件复制到【CodeEdge】文件夹下：\n接下来，将【CodeEdge】文件夹下的【hugo.toml】文件以及【content/post】路径下的【rich-content】文件夹一并删除：\n编辑【hugo.yaml】文件，将theme修改为themes文件夹下的主题文件夹同名：\n在命令行中再次执行【hugo server -D】\n访问【http://localhost:1313】就能加载添加的主题：\n新建Github博客网站仓库 在Github上新建一个博客网站仓库，如【CodeEdge.github.io】\n。\n为了连接的稳定性，github上最好选择ssh：\n在github的博客网站仓库依次执行以下步骤：\n编辑【hugo.yaml】文件，将baseurl修改为如下网站名称，与上面的步骤⑤保持一致：\n之后即可通过访问这个baseurl来查看部署的博客了。\n在命令行cd到public文件夹：\n依次执行以下命令：\n1 2 3 4 5 6 7 echo \u0026#34;# CodeEdge.github.io\u0026#34; \u0026gt;\u0026gt; README.md git init git add . git commit -m \u0026#34;first commit\u0026#34; git branch -M main git remote add origin git@github.com:github用户名/CodeEdge.github.io.git git push -u origin main 新建Github博客源代码仓库 在Github上新建一个博客源代码仓库，如【CodeEdge.github.io】\n。\n在命令行cd到CodeEdge文件夹：\n依次执行以下命令：\n1 2 3 4 5 6 7 echo \u0026#34;# CodeEdgeSource\u0026#34; \u0026gt;\u0026gt; README.md git init git add README.md git commit -m \u0026#34;first commit\u0026#34; git branch -M main git remote add origin git@github.com:github用户名/源代码仓库.git git push -u origin main 实现Github Action自动部署 在github上依次进行以下操作，创建token，需要勾选【repo】和【workflow】：\n将这个token保存到源代码仓库【CodeEdgeSource】中：\n在【CodeEdge】文件夹下新建以下文件：\nhugo_deploy文件的主要内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 name: Build and Deploy Hugo Site on: push: branches: - main paths: - \u0026#34;content/**/*.md\u0026#34; - \u0026#34;static/**\u0026#34; - \u0026#34;layouts/**\u0026#34; - \u0026#34;assets/**\u0026#34; workflow_dispatch: # 支持手动触发 concurrency: group: deploy-pages cancel-in-progress: true jobs: build-and-deploy: runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout source repository uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.DEPLOYCODEEDGE_TOKEN }} # 用于图片上传需要推送权限 - name: Git Configuration run: | git config --global core.quotePath false git config --global core.autocrlf false git config --global core.safecrlf true git config --global core.ignorecase false # ============ 构建 Hugo 站点 ============ - name: Setup Hugo uses: peaceiris/actions-hugo@v3 with: hugo-version: \u0026#34;latest\u0026#34; extended: true - name: Build site run: hugo -D # 包含 draft 文章 # ============ 部署到 GitHub Pages ============ - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v4 with: personal_token: ${{ secrets.DEPLOYCODEEDGE_TOKEN }} external_repository: github用户名/博客网站仓库 publish_branch: main publish_dir: ./public force_orphan: true commit_message: \u0026#34;auto deploy: ${{ github.event.head_commit.message }}\u0026#34; user_name: \u0026#34;github-actions[bot]\u0026#34; user_email: \u0026#34;github-actions[bot]@users.noreply.github.com\u0026#34; 在【CodeEdge】文件夹下新建【.gitignore】文件，避免提交以下非必要的文件：\n1 2 3 4 5 6 7 8 9 10 11 12 public resources .hugo_build.lock hugo.exe *.lock .DS_Store Thumbs.db *.tmp *.log .idea/ .vscode/ 博客部署步骤 1 2 3 4 5 6 7 8 9 10 11 12 #新建博客 hugo new content post/XXX.md #本地查看 hugo server -D #部署 git add . git commit -m \u0026#34;xxx\u0026#34; git push origin main 当新增的博客推送到源代码仓库后，GitHub Actions 会自动执行以下步骤：\n检测到推送 触发 .github/workflows/hugo_deploy.yaml 工作流 运行 hugo -D 构建网站 将生成的静态文件部署到博客网站仓库 等待一定的时间后，博客网站会自动更新 参考资料 1.配置ssh解决https不稳定的问题-CSDN博客\n2.下载 GitHub 仓库「单个文件夹」的方法_github只下载某个文件夹-CSDN博客\n3.推送错了仓库的解决办法-CSDN博客\n4.本地改乱了代码，如何恢复成和 GitHub 仓库一致的干净版本？-CSDN博客\n5.VS Code图形化界面操作Git-CSDN博客\n6.消除VS Code在检查style标签里面的 CSS 代码时产生的语法警告-CSDN博客\n小贴士 1.Hugo 需要 Front Matter 才能识别文章。也就是在文件最开头需要有以下这些代码：\n1 2 3 4 5 +++ title = \u0026#39;配置ssh解决https不稳定的问题\u0026#39; date = \u0026#39;2026-01-13T14:20:00+08:00\u0026#39; draft = false +++ 2.github账号现在有一个输入验证码的步骤，可在浏览器上安装【2FA Authenticator】插件获取。\n3.一些搭建博客可能用到的网站\n免费图标网站 https://icon-icons.com/ emoji词典 https://www.emojiall.com/ 在线PS工具 https://ps.pic.net/ ANI 鼠标样式转 PNG 文件 在线工具 https://ezgif.com/ani-to-apng PNG转ICO在线工具 https://www.png2ico.com/ 免费的商用字体 https://www.100font.com/ ","date":"2026-02-19T12:20:35Z","permalink":"https://iamxurulin.github.io/p/%E9%9B%B6%E6%88%90%E6%9C%AC%E4%BB%8E0%E5%88%B01%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2/","title":"零成本从0到1搭建个人博客"},{"content":"命令行操作git有一个比较大的缺点就是，写多行注释的时候不太方便，可以考虑使用VS Code的图形化界面操作。\n打开 VS Code，左侧点击【源代码管理】图标，也就是下图的①，在这里能看到所有修改过的文件； 点击面板顶部的【+】，就等价于敲的 git add .，把所有修改加入到暂存区； 如果只想提交部分文件，只需要单独点击某个文件右侧的【+】即可； 在面板顶部的【消息】输入框里，这里就可以直接换行写注释，想写多少行写多少行； 写完注释后，点击输入框【提交】按钮，Git 就完成提交了； 提交后，点击下图中的⑤，就可以直接把提交【推送】到远程的 main 分支了。 小贴士 1.【提交】完成后，面板上会出现一个【同步更改】按钮，点击这个按钮的话VS Code 会先执行 git pull（拉取）再执行 git push（推送），如果只需要执行推送的话，就不要点击这个按钮，直接点击图中的那个⑤。\n2.提交注释的行业规范前缀：\nfeat: 新增功能（比如：feat: 新增用户登录功能） fix: 修复 bug（比如：fix: 修复列表页数据为空的 bug） docs: 只修改文档 / 注释（比如：docs: 更新接口文档说明） style: 只改代码格式（比如：style: 格式化代码缩进，无逻辑修改） refactor: 代码重构（比如：refactor: 重构登录模块的逻辑） test: 新增 / 修改测试代码（比如：test: 新增登录功能的单元测试） ","date":"2026-02-19T00:39:53Z","permalink":"https://iamxurulin.github.io/p/vs-code%E5%9B%BE%E5%BD%A2%E5%8C%96%E7%95%8C%E9%9D%A2%E6%93%8D%E4%BD%9Cgit/","title":"VS Code图形化界面操作Git"},{"content":"⚠️注意：以下操作会永久删除本地未提交的修改，请确保是已经真的不需要这些代码了！\n情况1 在本地改了代码、删了文件、新增了文件，但是从来没有执行过 git add（暂存）、 git commit （提交） 。\n执行一行命令即可：\n1 git checkout . 情况2 情况1的命令只会恢复修改/删除的文件，不会删除本地新增的、从来没提交过的文件，如果想把这些新增的无用文件也一起删掉，恢复到绝对干净的状态，执行以下命令：\n1 git clean -fd 小贴士：\n1.情况1的命令只负责还原 Git 认识的文件（修改过的、删除过的）。它不敢随便删除新创建的文件，因为它不知道那些新文件是不是重要数据。\n2.情况2的命令，-f是强制删除文件；-d 是强制删除文件夹，执行后本地无任何多余文件。它只负责删除 Git 不认识的文件（新增加的）。它不管那些旧文件改没改。\n","date":"2026-02-18T23:48:28Z","permalink":"https://iamxurulin.github.io/p/%E6%9C%AC%E5%9C%B0%E6%94%B9%E4%B9%B1%E4%BA%86%E4%BB%A3%E7%A0%81%E5%A6%82%E4%BD%95%E6%81%A2%E5%A4%8D%E6%88%90%E5%92%8C-github-%E4%BB%93%E5%BA%93%E4%B8%80%E8%87%B4%E7%9A%84%E5%B9%B2%E5%87%80%E7%89%88%E6%9C%AC/","title":"本地改乱了代码，如何恢复成和 GitHub 仓库一致的干净版本？"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); in.nextToken(); int n = (int)in.nval; out.println(find(n)); out.flush(); out.close(); br.close(); } private static int find(int n) { int n2 = 0, n5 = 0; int p = 1; for(int i=2;i\u0026lt;=n;i++){ int tmp = i; while (tmp%5==0) { n5++; tmp/=5; } while (tmp%2==0) { n2++; tmp/=2; } p=(p*tmp)%10; } for(int i=0;i\u0026lt;n2-n5;i++){ p=(p*2)%10; } return p; } ","date":"2026-02-18T21:33:04Z","permalink":"https://iamxurulin.github.io/p/bishi59-%E9%98%B6%E4%B9%98%E6%9C%AB%E5%B0%BE%E9%9D%9E%E9%9B%B6%E6%95%B0%E5%AD%97/","title":"BISHI59 阶乘末尾非零数字"},{"content":" 思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); in.nextToken(); int n = (int)in.nval; long res = n; while (true) { int a = find(n); n/=a; if(n==1){ res+=1; break; }else{ res+=n; } } out.println(res); out.flush(); out.close(); br.close(); } private static int find(int a) { if(a%2==0){ return 2; } for(int i=3;i*i\u0026lt;=a;i++){ if(a%i==0){ return i; } } return a; } ","date":"2026-02-18T20:38:20Z","permalink":"https://iamxurulin.github.io/p/bishi58-%E7%9F%A9%E5%BD%A2%E6%B8%B8%E6%88%8F/","title":"BISHI58 矩形游戏"},{"content":"\n###思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); String[] str = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); Long a = Long.parseLong(str[0]); Long b = Long.parseLong(str[1]); long g = gcd(a,b); long l = (a==0||b==0)?0:(a/g)*b; out.println(g+\u0026#34; \u0026#34;+l); out.flush(); out.close(); br.close(); } private static long gcd(long a, long b) { while (b != 0) { long tmp = b; b = a%b; a=tmp; } return a; } ","date":"2026-02-17T21:06:10Z","permalink":"https://iamxurulin.github.io/p/bishi57-%E6%9C%80%E5%A4%A7%E5%85%AC%E5%9B%A0%E6%95%B0%E4%B8%8E%E6%9C%80%E5%B0%8F%E5%85%AC%E5%80%8D%E6%95%B0/","title":"BISHI57 最大公因数与最小公倍数"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); Long n = Long.parseLong(br.readLine().trim()); for (long i = 2; i * i \u0026lt;= n; i++) { while (n % i == 0) { out.print(i+\u0026#34; \u0026#34;); n/=i; } } if(n\u0026gt;1){ out.print(n+\u0026#34; \u0026#34;); } out.flush(); out.close(); br.close(); } 小贴士 后面那个if语句主要是处理分解后剩余的、大于√n 的质因数。\n如果不加这个判断，会导致部分质数或大质因数无法被输出，最终分解结果不完整。\n因为代码中外层循环的终止条件是 i * i \u0026lt;= n，只检查到√n，\n如果一个数 n 能被分解为 a × b（a ≤ b），则 a 必然 ≤ √n，b 必然 ≥ √n。\n因此只需检查到√n，就能找到所有小质因数，剩余的未分解部分要么是 1，要么是一个大于√n 的质数。\n","date":"2026-02-17T20:24:50Z","permalink":"https://iamxurulin.github.io/p/bishi56-%E5%88%86%E8%A7%A3%E8%B4%A8%E5%9B%A0%E6%95%B0/","title":"BISHI56 分解质因数"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); Long n = Long.parseLong(br.readLine().trim()); if (isPrime(n)) { out.println(\u0026#34;Yes\u0026#34;); } else { out.println(\u0026#34;No\u0026#34;); } out.flush(); out.close(); br.close(); } private static boolean isPrime(long n) { if (n == 1) { return false; } for (long i = 2; i * i \u0026lt;= n; i++) { if (n % i == 0) { return false; } } return true; } ","date":"2026-02-17T19:48:07Z","permalink":"https://iamxurulin.github.io/p/bishi55-%E5%88%A4%E6%96%AD%E8%B4%A8%E6%95%B0/","title":"BISHI55 判断质数"},{"content":"\n流程图 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 static class Goods{ long w; long v; long c; Goods(long w,long v,long c){ this.w = w; this.v = v; this.c = c; } } public static void main(String[] args) throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int n = Integer.parseInt(br.readLine().trim()); Goods[] goods = new Goods[n]; long total = 0; for(int i=0;i\u0026lt;n;i++){ String[] str = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); long w = Long.parseLong(str[0]); long v = Long.parseLong(str[1]); long c = Long.parseLong(str[2]); goods[i]=new Goods(w, v,c); total += v; } Arrays.sort(goods,(x,y)-\u0026gt;Long.compare(x.c*y.w, y.c*x.w)); // 总实际体积 = 初始体积总和 - sumCW，因此最大化sumCW是实现最小体积的关键 long sumCW = 0; // 记录当前遍历商品的「上方总重量」，初始为0（第一个商品在最顶部，上方无重量） long upperWeight = 0; // 按\u0026#34;从上到下\u0026#34;的堆叠顺序遍历排序后的商品 for (Goods g : goods) { // 计算当前商品的压缩贡献值：c_i × 上方总重量（W_i） // 这里必须先计算贡献，再更新上方重量（避免把当前商品算入自己的上方） sumCW += g.c * upperWeight; // 更新上方总重量：当前商品会成为后续商品的\u0026#34;上方重量\u0026#34;，需累加其重量 upperWeight += g.w; } long res = total -sumCW; out.println(res); out.flush(); out.close(); br.close(); } ","date":"2026-02-16T22:00:34Z","permalink":"https://iamxurulin.github.io/p/bishi54%E8%B4%A7%E7%89%A9%E5%A0%86%E6%94%BE/","title":"BISHI54货物堆放"},{"content":" 流程图 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 static class Minister{ long a; long b; Minister(long a,long b){ this.a = a; this.b = b; } } public static void main(String[] args) throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int n = Integer.parseInt(br.readLine().trim()); String[] str = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); long a0 = Long.parseLong(str[0]); Minister[] ministers = new Minister[n]; for(int i=0;i\u0026lt;n;i++){ String[] ministerStr = br.readLine().trim().split(\u0026#34;\\\\s+\u0026#34;); long a = Long.parseLong(ministerStr[0]); long b = Long.parseLong(ministerStr[1]); ministers[i]=new Minister(a, b); } Arrays.sort(ministers,(x,y)-\u0026gt;Long.compare(x.a*x.b, y.a*y.b)); // 计算最大金币数 long maxCoin = 0; long productA = a0; // 前序左手数的乘积（初始为国王的a0） for (Minister m : ministers) { // 当前大臣的金币数 = 前序乘积 / 当前b long coin = productA / m.b; // 更新最大金币数 if (coin \u0026gt; maxCoin) { maxCoin = coin; } // 前序乘积 *= 当前大臣的a productA *= m.a; } out.println(maxCoin); out.flush(); out.close(); br.close(); } ","date":"2026-02-16T20:53:12Z","permalink":"https://iamxurulin.github.io/p/bishi53-p1080-%E5%9B%BD%E7%8E%8B%E6%B8%B8%E6%88%8F%E7%AE%80%E5%8C%96%E7%89%88/","title":"BISHI53 [P1080] 国王游戏(简化版)"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int n = Integer.parseInt(br.readLine()); String[] str = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); long[] p = new long[n]; for (int i = 0; i \u0026lt; n; i++) { p[i] = Long.parseLong(str[i]); } PriorityQueue\u0026lt;Long\u0026gt; minHeap = new PriorityQueue\u0026lt;\u0026gt;(); // 小顶堆：堆顶始终是当前堆中最小值 long profit = 0; // 初始化总利润为0 for (int i = 0; i \u0026lt; n; i++) { minHeap.add(p[i]); // 将当前价格加入堆 if (minHeap.peek() \u0026lt; p[i]) { // 如果堆顶（当前最低价格）\u0026lt; 当前价格 → 可以低买高卖 long buy = minHeap.poll(); // 取出堆顶（买入价） profit += p[i] - buy; // 累加本次利润（当前价-买入价） minHeap.add(p[i]); // 把当前价格重新入堆（相当于“卖出后再以当前价买入”，继续后续交易） } } out.println(profit); out.flush(); out.close(); br.close(); } ","date":"2026-02-15T20:24:29Z","permalink":"https://iamxurulin.github.io/p/bishi51-%E4%BD%8E%E4%B9%B0%E9%AB%98%E5%8D%96/","title":"BISHI51 低买高卖"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int n = Integer.parseInt(br.readLine()); // 存储每个建筑的维修耗时t和报废时限d int[][] buildings = new int[n][2]; for (int i = 0; i \u0026lt; n; i++) { String[] numStr = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); buildings[i][0] = Integer.parseInt(numStr[0]); buildings[i][1] = Integer.parseInt(numStr[1]); } // 按报废时限d_i升序排序（优先处理时限早的） Arrays.sort(buildings, (a, b) -\u0026gt; a[1] - b[1]); // 初始化最大堆（存储已选建筑的耗时，优先弹出最大值） PriorityQueue\u0026lt;Integer\u0026gt; maxHeap = new PriorityQueue\u0026lt;\u0026gt;(Collections.reverseOrder()); long total = 0; // 当前累计维修时间 for (int[] b : buildings) { int t = b[0]; int d = b[1]; // 加入当前建筑，累计时间 maxHeap.add(t); total += t; // 若累计时间超过当前建筑的时限，移除耗时最长的建筑 if (total \u0026gt; d) { int removeT = maxHeap.poll(); total -= removeT; } } // 堆的大小即为最多可维修的建筑数量 out.println(maxHeap.size()); out.flush(); out.close(); br.close(); } ","date":"2026-02-15T00:47:10Z","permalink":"https://iamxurulin.github.io/p/bishi50-jsoi2007%E5%BB%BA%E7%AD%91%E6%8A%A2%E4%BF%AE/","title":"BISHI50 [JSOI2007]建筑抢修"},{"content":" 思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); String[] str = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); int n = Integer.parseInt(str[0]); int k = Integer.parseInt(str[1]); String[] numStr = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); long[] a = new long[n]; long total = 0; for (int i = 0; i \u0026lt; n; i++) { a[i] = Long.parseLong(numStr[i]); total += a[i];// 累加总耗时 } // 初始化最大堆（优先队列），用于存储可跳过的关卡耗时（优先弹出最大值） // PriorityQueue默认是最小堆，Collections.reverseOrder()转为最大堆 PriorityQueue\u0026lt;Long\u0026gt; maxHeap = new PriorityQueue\u0026lt;\u0026gt;(Collections.reverseOrder()); long saveTime = 0; for (int i = n - 1; i \u0026gt;= 0; i--) { // (i+1)表示\u0026#34;已通过前i+1个关卡\u0026#34;（按顺序），满足每k个关卡则获得1个道具 // 0-based索引转1-based计数，确保规则匹配 if ((i + 1) % k == 0 \u0026amp;\u0026amp; !maxHeap.isEmpty()) { // 获得道具，跳过堆中耗时最大的关卡，累加跳过的耗时 saveTime += maxHeap.poll(); } // 最小总耗时 = 全通关耗时 - 跳过的最大耗时之和 maxHeap.add(a[i]); } out.println(total - saveTime); out.flush(); out.close(); br.close(); } ","date":"2026-02-15T00:11:29Z","permalink":"https://iamxurulin.github.io/p/bishi49-%E5%B0%8F%E7%BA%A2%E9%97%AF%E5%85%B3/","title":"BISHI49 小红闯关"},{"content":"\n思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); String[] str = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); int n = Integer.parseInt(str[0]); int k = Integer.parseInt(str[1]); String[] numStr = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); int[] a = new int[n]; for (int i = 0; i \u0026lt; n; i++) { a[i] = Integer.parseInt(numStr[i]); } Arrays.sort(a); long maxScore = 0; int i = n - 1;// 从末尾开始遍历 // 从后往前贪心配对 while (i \u0026gt;= 1) { if (a[i] - a[i - 1] \u0026lt;= k) { maxScore += (long) a[i] * a[i - 1]; i -= 2;// 配对成功，跳过这两个元素 } else { i -= 1;// 无法配对，只跳过当前元素 } } out.println(maxScore); out.flush(); out.close(); br.close(); } ","date":"2026-02-14T22:24:11Z","permalink":"https://iamxurulin.github.io/p/%E8%B4%AA%E5%BF%83bishi48-%E5%B0%8F%E7%BA%A2%E7%9A%84%E6%95%B4%E6%95%B0%E9%85%8D%E5%AF%B9/","title":"【贪心】BISHI48 小红的整数配对"},{"content":" 思路 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int t = Integer.parseInt(br.readLine()); while (t-- \u0026gt; 0) { String str = br.readLine(); char[] s = str.toCharArray(); int n = s.length; for (int i = 0; i \u0026lt; n; i++) { int max_val = s[i] - \u0026#39;0\u0026#39;; int max_index = i; // 在窗口内寻找能产生最大价值的来源 for (int j = i; j \u0026lt; Math.min(i + 10, n); j++) { int cur_val = (s[j] - \u0026#39;0\u0026#39;) - (j - i); if (cur_val \u0026gt; max_val) { max_val = cur_val; max_index = j; } } // 物理移动来源字符到当前位置i int tmp_index = max_index; while (tmp_index \u0026gt; i) { char tmp_val = s[tmp_index]; s[tmp_index] = s[tmp_index - 1]; s[tmp_index - 1] = tmp_val; tmp_index--; } // 将当前位置i的字符值更新为计算出的最优值 s[i] = (char) (max_val + \u0026#39;0\u0026#39;); } out.println(new String(s)); } out.flush(); out.close(); br.close(); } ","date":"2026-02-14T21:39:48Z","permalink":"https://iamxurulin.github.io/p/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3bishi47-%E4%BA%A4%E6%8D%A2%E5%88%B0%E6%9C%80%E5%A4%A7/","title":"【滑动窗口】BISHI47 交换到最大"},{"content":"\n流程图 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int n = Integer.parseInt(br.readLine()); String[] str = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); int[] redStr = new int[str.length]; for (int i = 0; i \u0026lt; str.length; i++) { redStr[i] = Integer.parseInt(str[i]); } int sum = 0; // 遍历每种药剂 for (int i = 0; i \u0026lt; n; i++) { // 读取合成第i种蓝色药剂所需的两种红色药剂的编号b和c String[] split = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); int b = Integer.parseInt(split[0]); int c = Integer.parseInt(split[1]); // 计算合成第i种蓝色药剂的花费，即所需两种红色药剂价格之和 int blue = redStr[b - 1] + redStr[c - 1]; // 获取直接购买第i种红色药剂的花费 int red = redStr[i]; // 选择合成蓝色药剂和直接购买红色药剂两者花费较小的值，累加到总花费sum中 sum += Math.min(blue, red); } out.println(sum); out.close(); br.close(); } ","date":"2026-02-14T20:09:43Z","permalink":"https://iamxurulin.github.io/p/bishi46-%E5%B0%8F%E7%BA%A2%E7%9A%84%E9%AD%94%E6%B3%95%E8%8D%AF%E5%89%82/","title":"BISHI46 小红的魔法药剂"},{"content":" 流程图 纵向遍历矩阵统计 贪心排序与 Score 计算 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public static void main(String[] args)throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); String[] str = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); int n = Integer.parseInt(str[0]); int m = Integer.parseInt(str[1]); int k = Integer.parseInt(str[2]); char[][] matrix = new char[n][m]; for(int i=0;i\u0026lt;n;i++){ matrix[i]=br.readLine().toCharArray(); } List\u0026lt;Integer\u0026gt; block = new ArrayList\u0026lt;\u0026gt;(); for(int j = 0;j\u0026lt;m;j++){ int current = 0; for(int i=0;i\u0026lt;n;i++){ if(matrix[i][j]==\u0026#39;o\u0026#39;){ current++; }else{ if(current\u0026gt;=2){ block.add(current); } current = 0; } } if(current\u0026gt;=2){ block.add(current); } } Collections.sort(block,Collections.reverseOrder()); int score = 0; for(int len:block){ if(k==0){ break; } int cell = Math.min(k, len); if(cell\u0026gt;=2){ score += cell-1; } k-=cell; } out.println(score); out.flush(); out.close(); br.close(); } ","date":"2026-02-13T21:59:29Z","permalink":"https://iamxurulin.github.io/p/bishi45-%E5%B0%8F%E7%BA%A2%E7%9A%84%E7%9F%A9%E9%98%B5%E6%9F%93%E8%89%B2/","title":"BISHI45 小红的矩阵染色"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int n = Integer.parseInt(br.readLine()); String[] str = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); long sum = 0; int minOdd = Integer.MAX_VALUE; for (String s : str){ int num = Integer.parseInt(s); sum += num; if((num\u0026amp;1)==1){ minOdd=Math.min(num, minOdd); } } long res; if((sum\u0026amp;1)==0){ res = sum; }else{ if(minOdd!=Integer.MAX_VALUE){ res = sum - minOdd;// 总和是奇数，需要减去最小的奇数（若存在） }else{ res = 0;// 没有奇数，只能选空背包 } } out.println(res); out.flush(); out.close(); br.close(); } ","date":"2026-02-13T00:18:28Z","permalink":"https://iamxurulin.github.io/p/%E9%A2%98%E8%A7%A3-%E7%81%B5%E5%BC%82%E8%83%8C%E5%8C%85/","title":"题解 | 灵异背包？"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); String[] s = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); int n = Integer.parseInt(s[0]); long x = Long.parseLong(s[1]); String[] strA = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); String[] strB = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); long sum = 0; for (int i = 0; i \u0026lt; n; i++) { int a = Integer.parseInt(strA[i]); int b = Integer.parseInt(strB[i]); sum += Math.min(a, b); // 每种货物选A/B中更便宜的 } long res = Math.min(x, sum);// 比较单独买总和与网购成本 out.println(res); out.flush(); out.close(); br.close(); } ","date":"2026-02-13T00:00:30Z","permalink":"https://iamxurulin.github.io/p/bishi43-%E8%AE%A8%E5%8E%8C%E9%AC%BC%E8%BF%9B%E8%B4%A7/","title":"BISHI43 讨厌鬼进货"},{"content":"\n求解思路 由于对任意正整数$k, i$，有$k \\mod i = k - i \\times \\lfloor \\frac{k}{i} \\rfloor$\n$$\\sum_{i=1}^n (k \\mod i) = \\sum_{i=1}^n \\left( k - i \\times \\lfloor \\frac{k}{i} \\rfloor \\right) = n \\times k - \\sum_{i=1}^{\\min(n,k)} \\left( i \\times \\lfloor \\frac{k}{i} \\rfloor \\right)$$求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); String[] str = br.readLine().split(\u0026#34;\\\\s+\u0026#34;); long n = Long.parseLong(str[0]); long k = Long.parseLong(str[1]); long total = n * k; // 原表达式的第一部分 long m = Math.min(n, k); // 只需要计算到min(n,k) long i = 1; while (i \u0026lt;= m) { long v = k / i; long j = Math.min(k / v, m); // 当前块的右边界 // 计算i到j的和：(i+j)*(j-i+1)/2，再乘以v total -= v * (i + j) * (j - i + 1) / 2; i = j + 1; // 跳到下一个块 } out.println(total); out.flush(); out.close(); br.close(); } ","date":"2026-02-12T23:18:07Z","permalink":"https://iamxurulin.github.io/p/bishi42-%E4%BD%99%E6%95%B0%E6%B1%82%E5%92%8C/","title":"BISHI42 余数求和"},{"content":"\n求解思路 当 $n=5$ 时：\n$i=1 \\rightarrow \\lfloor 5/1 \\rfloor = 5$ $i=2 \\rightarrow \\lfloor 5/2 \\rfloor = 2$ $i=3 \\rightarrow \\lfloor 5/3 \\rfloor = 1$ $i=4 \\rightarrow \\lfloor 5/4 \\rfloor = 1$ $i=5 \\rightarrow \\lfloor 5/5 \\rfloor = 1$ 可以看到：$i=3,4,5$ 对应的 $\\lfloor 5/i \\rfloor$ 都是 1，这一段可以批量计算。\n对于当前的 $i$，设 $v = \\lfloor \\frac{n}{i} \\rfloor$，则最大的 $j$ 满足 $\\lfloor \\frac{n}{j} \\rfloor = v$ 是： $j = \\left\\lfloor \\frac{n}{v} \\right\\rfloor$。\n因此，我们可以按“值相同的区间”分块，每个区间的贡献是 $v \\times (j - i + 1)$，然后直接跳到 $j+1$ 继续计算。\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer in = new StringTokenizer(br.readLine()); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); long n = Long.parseLong(in.nextToken()); long res = 0; long i = 1; while (i \u0026lt;= n) { long v = n / i; // 当前块的取值 long j = n / v; // 当前块的右边界 res += v * (j - i + 1); // 计算当前块的贡献 i = j + 1; // 跳到下一个块的起点 } out.println(res); out.flush(); out.close(); br.close(); } ","date":"2026-02-12T22:28:33Z","permalink":"https://iamxurulin.github.io/p/bishi41-%E6%A8%A1%E6%9D%BF%E6%95%B4%E9%99%A4%E5%88%86%E5%9D%97/","title":"BISHI41 【模板】整除分块"},{"content":"\n求解思路 1.先分别计算序列A和B的总和sumA 和sumB\n2.将所有元素按照$a_i +b_i$从大到小排序\n3.依次选取排序后的元素，累加子集的A和、B和，直到同时满足超过总和的一半\n4.输出选取的元素的原始索引\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 static class Element { int a; // 对应数组A的元素值 int b; // 对应数组B的元素值 int index;// 该元素在原数组中的索引（从1开始） // 构造方法：初始化a、b、index Element(int a, int b, int index) { this.a = a; this.b = b; this.index = index; } } public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int n = Integer.parseInt(br.readLine()); String[] strA = br.readLine().split(\u0026#34; \u0026#34;); String[] strB = br.readLine().split(\u0026#34; \u0026#34;); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); List\u0026lt;Element\u0026gt; elements = new ArrayList\u0026lt;\u0026gt;(); long sumA = 0, sumB = 0; for (int i = 0; i \u0026lt; n; i++) { int a = Integer.parseInt(strA[i]); // 把strA的字符串转成整数 int b = Integer.parseInt(strB[i]); // 把strB的字符串转成整数 elements.add(new Element(a, b, i + 1)); // 封装成Element，索引从1开始 sumA += a; // 累加A的总和 sumB += b; // 累加B的总和 } Collections.sort(elements, (e1, e2) -\u0026gt; { long s1 = (long) e1.a + e1.b; long s2 = (long) e2.a + e2.b; return Long.compare(s2, s1);// 按a+b从大到小排序 }); List\u0026lt;Integer\u0026gt; res = new ArrayList\u0026lt;\u0026gt;(); long currentA = 0, currentB = 0; for (Element e : elements) { res.add(e.index); // 把当前元素的索引加入结果 currentA += e.a; // 累加子集的A和 currentB += e.b; // 累加子集的B和 // 检查是否满足条件：子集和超过总和的一半 if (currentA \u0026gt; sumA / 2 \u0026amp;\u0026amp; currentB \u0026gt; sumB / 2) { break; // 满足条件则停止选取 } } out.println(res.size()); for (int i = 0; i \u0026lt; res.size(); i++) { out.print(res.get(i)+(i == res.size() - 1 ? \u0026#34;\u0026#34; : \u0026#34; \u0026#34;)); } out.flush(); out.close(); br.close(); } ","date":"2026-02-12T21:12:37Z","permalink":"https://iamxurulin.github.io/p/bishi40%E6%95%B0%E7%BB%84%E5%8F%96%E7%B2%BE/","title":"BISHI40数组取精"},{"content":"在项目中想通过引入Redisson实现限流，运行RedisLimiterManagerTest.doRateLimit测试方法时，弹出了这样的提示：\n原因分析 当项目依赖较多时，IDEA运行测试需要加载的类路径（各种依赖包的路径）会变得很长，而Windows对命令行参数的长度有默认限制（通常是8191字符），超出后就会触发“命令行过长”的错误。\n解决方法 1.点击IDEA右上角的运行配置下拉框 ➡️选择【编辑配置】，打开运行/调试配置窗口：\n2.在配置窗口右侧，找到并点击【修改选项(M) Alt+M】按钮，弹出选项菜单后，选择 【缩短命令行】，此时界面会出现【缩短命令行】的下拉框，直接选择【JAR 清单】，这样的话IDEA会通过生成临时JAR清单来管理类路径。\n3.最后点击窗口右下角的【应用】➡️再点击【确定】，保存配置。\n4.重新运行RedisLimiterManagerTest.doRateLimit测试方法，能正常执行并输出“成功”日志，命令行过长的问题解决~\n","date":"2026-02-11T12:44:33Z","permalink":"https://iamxurulin.github.io/p/idea%E8%BF%90%E8%A1%8Cspringboot%E6%B5%8B%E8%AF%95%E6%8A%A5%E9%94%99%E5%91%BD%E4%BB%A4%E8%A1%8C%E8%BF%87%E9%95%BF/","title":"IDEA运行SpringBoot测试报错“命令行过长”？"},{"content":"进入到Redis的安装目录：\n双击【redis-server.exe】,如果双击之后窗口一闪而过， 检查一下默认的6379端口是否被占用：\n1 2 # 查看 6379 端口是否被占用 netstat -ano | findstr 6379 如果输出类似下面这样的信息：\n1 2 3 # 类似这样的输出: TCP 127.0.0.1:6379 0.0.0.0:0 LISTENING 12345 ↑这是进程ID 说明 6379 端口已被进程占用，再检查是否已有 Redis 服务在运行：\n1 2 # 查看 Redis 服务状态 sc query Redis 如果输出：\n1 2 3 SERVICE_NAME: Redis TYPE : 10 WIN32_OWN_PROCESS STATE : 4 RUNNING 说明Redis 已经作为 Windows 服务 在后台运行了，导致了端口被占用。\n接下来，直接使用已经运行的 Redis，直接双击安装目录下的【redis-cli.exe】，在弹出的窗口输入【config get requirepass】,如果以下的（2）为空，表示 没有设置密码。\n","date":"2026-02-11T11:50:41Z","permalink":"https://iamxurulin.github.io/p/%E6%80%8E%E4%B9%88%E7%9F%A5%E9%81%93%E6%9C%AC%E5%9C%B0%E7%9A%84redis%E6%9C%89%E6%B2%A1%E6%9C%89%E8%AE%BE%E7%BD%AE%E5%AF%86%E7%A0%81/","title":"怎么知道本地的Redis有没有设置密码"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer in = new StringTokenizer(br.readLine()); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int t = Integer.parseInt(in.nextToken()); for (int i = 0; i \u0026lt; t; i++) { in = new StringTokenizer(br.readLine()); String s = in.nextToken(); StringBuilder sb1 = new StringBuilder(); StringBuilder sb2 = new StringBuilder(); boolean flag = true; for (char c : s.toCharArray()) { int digit = c - \u0026#39;0\u0026#39;; int half = digit / 2; //当前位是偶数 → 平分给sb1和sb2 if (digit % 2 == 0) { sb1.append(half); sb2.append(half); } else { //当前位是奇数 → 交替分配多的1（half+1） if (flag) { sb1.append(half + 1); sb2.append(half); } else { sb1.append(half); sb2.append(half + 1); } flag = !flag; } } out.println(Long.parseLong(sb1.toString()) + \u0026#34; \u0026#34; + Long.parseLong( sb2.toString())); } out.flush(); out.close(); br.close(); } ","date":"2026-02-11T00:02:55Z","permalink":"https://iamxurulin.github.io/p/%E6%95%B0%E4%BD%8D%E5%B7%AE%E4%B8%8E%E6%95%B0%E5%80%BC%E5%92%8C%E7%9A%84%E6%9E%84%E9%80%A0/","title":"数位差与数值和的构造"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); in.nextToken(); int t = (int) in.nval; for (int i = 0; i \u0026lt; t; i++) { in.nextToken(); int n = (int) in.nval; in.nextToken(); int m = (int) in.nval; if(n%(m+1)==0){ out.println(\u0026#34;NO\u0026#34;); }else{ out.println(\u0026#34;YES\u0026#34;); } } out.flush(); out.close(); br.close(); } 小贴士 若 n = k*(m+1)（即 n%(m+1)==0）：无论先手取 x 个（1≤x≤m），后手都可以取 (m+1)-x 个，最终后手总能取到最后一个物品 ➡️ 先手必败；\n若 n ≠ k*(m+1)：先手可以先取 n%(m+1) 个，让剩余物品数为 k*(m+1)，此时后手陷入必败局面➡️先手必胜。\n","date":"2026-02-10T22:52:34Z","permalink":"https://iamxurulin.github.io/p/bishi35-%E6%A8%A1%E6%9D%BF%E5%B7%B4%E4%BB%80%E5%8D%9A%E5%BC%88/","title":"BISHI35 【模板】巴什博弈"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); in.nextToken(); int t = (int) in.nval; for (int i = 0; i \u0026lt; t; i++) { in.nextToken(); int n = (int) in.nval; // 判断规则：n=3、n=6，或 n≥9 且为奇数 → Bob 赢，否则 Alice 赢； if (n == 3 || n == 6 || (n \u0026gt;= 9 \u0026amp;\u0026amp; (n % 2 == 1))) { out.println(\u0026#34;Bob\u0026#34;); } else { out.println(\u0026#34;Alice\u0026#34;); } } out.flush(); out.close(); br.close(); } ","date":"2026-02-10T21:59:29Z","permalink":"https://iamxurulin.github.io/p/bishi34-%E7%94%9C%E8%9C%9C%E7%9A%84%E5%8D%9A%E5%BC%88/","title":"BISHI34 甜蜜的博弈"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // 拼接所有输入行：解决“数字可换行/空格分隔”的ACM输入特性 StringBuilder sb = new StringBuilder(); String line; // 逐行读取所有输入，直到输入结束（br.readLine()返回null） while ((line = br.readLine()) != null) { // trim()去掉行首行尾空格，append(\u0026#34; \u0026#34;)保证数字间用空格分隔 sb.append(line.trim()).append(\u0026#34; \u0026#34;); } // 拼接后的字符串按“任意空格”拆分成一个个数字字符串 StringTokenizer in = new StringTokenizer(sb.toString()); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int t = Integer.parseInt(in.nextToken()); for (int i = 0; i \u0026lt; t; i++) { long m = Long.parseLong(in.nextToken()); long query = Long.parseLong(in.nextToken()); long res = 0; for (int j = 0; j \u0026lt; m; j++) { // 读取第 j 个数值 val long val = Long.parseLong(in.nextToken()); // 非第一个数，res 只保留和当前 val 二进制位 “都为 1” 的部分，其他位全部变成 0 res = j \u0026gt; 0 ? ~(res ^ val) \u0026amp; res : val; // 非第一个数且 res\u0026gt;0 时，左移1位（等价于×2） if (res \u0026gt; 0 \u0026amp;\u0026amp; j \u0026gt; 0) { res \u0026lt;\u0026lt;= 1; } } int left = Integer.parseInt(in.nextToken()); int right = Integer.parseInt(in.nextToken()); out.println(res); res = 0; } out.flush(); out.close(); br.close(); } 小贴士 反码：原码除符号位外全部取反\n负数的补码 = 反码 + 1\n~(res^val)\u0026amp;res的本质是筛选出 res 和 val 二进制中 “都为 1” 的位\n","date":"2026-02-10T21:20:54Z","permalink":"https://iamxurulin.github.io/p/bishi33-poi-%E7%9A%84%E6%96%B0%E5%8A%A0%E6%B3%95easy-version/","title":"BISHI33 Poi 的新加法（Easy Version）"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); String str = br.readLine(); long n =Long.parseLong(str.trim()); int count = 0; while (n \u0026gt; 0) { count += n \u0026amp; 1; n = n \u0026gt;\u0026gt; 1; } out.println(count); out.flush(); out.close(); br.close(); } 小贴士 StreamTokenizer 的 in.nval 是 double 类型（64 位浮点数）， 但 double 的有效整数精度只有 53 位，将其强转为 long（64 位整数）会造成精度丢失，所以这里需要用BufferedReader.readLine()读取字符串再转 long。\n","date":"2026-02-09T22:09:39Z","permalink":"https://iamxurulin.github.io/p/bishi30-%E4%BA%8C%E8%BF%9B%E5%88%B6%E6%95%B01/","title":"BISHI30 二进制数1"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); in.nextToken(); int n = (int) in.nval; int[] arr = new int[n]; if(n\u0026lt;=2){ out.println(-1); }else{ arr[0]=3;// 第一个元素固定为3 for (int i = 1; i \u0026lt; n; i++) { arr[i]=i+1;//从第2个元素开始，值为「索引+1」 } arr[2]=1;// 第三个元素强制改为1 for(int i=0;i\u0026lt;n;i++){ out.print(arr[i]+(i == n - 1 ? \u0026#34;\u0026#34; : \u0026#34; \u0026#34;)); } } out.flush(); out.close(); br.close(); } ","date":"2026-02-09T21:12:30Z","permalink":"https://iamxurulin.github.io/p/bishi29-%E5%B0%8F%E7%BA%A2%E7%9A%84%E6%8E%92%E5%88%97%E6%9E%84%E9%80%A0/","title":"BISHI29 小红的排列构造①"},{"content":" 循环矩阵 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); in.nextToken(); int n = (int) in.nval; in.nextToken(); int k = (int) in.nval; int[] firstRow = new int[n]; int q = k/n; int r = k%n; /** * sum(firstRow) = r*(q+1) + (n-r)*q = n*q + r = k */ for(int i=0;i\u0026lt;n;i++){ if(i\u0026lt;r){ firstRow[i]=q+1; }else{ firstRow[i]=q; } } for(int i=0;i\u0026lt;n;i++){ for(int j=0;j\u0026lt;n;j++){ //第i行是第一行循环左移 i 位的结果 //加n可避免j-i为负数时取模出错 out.print(firstRow[(j-i+n)%n]+(j==n-1?\u0026#34;\u0026#34;:\u0026#34; \u0026#34;)); } out.println(); } out.flush(); out.close(); br.close(); } 方法2-YYDS（有一点马叉虫） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); in.nextToken(); int n = (int) in.nval; in.nextToken(); int k = (int) in.nval; for (int i = 0; i \u0026lt; n; i++) { for (int j = 0; j \u0026lt; n; j++) { if (i == j) { out.print(k + (j == n - 1 ? \u0026#34;\u0026#34; : \u0026#34; \u0026#34;)); } else { out.print(0 + (j == n - 1 ? \u0026#34;\u0026#34; : \u0026#34; \u0026#34;)); } } out.println(); } out.flush(); out.close(); br.close(); } ","date":"2026-02-09T20:25:17Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%A4%E7%A7%8D%E6%96%B9%E5%BC%8F%E6%9E%84%E9%80%A0%E6%95%B0%E7%8B%AC/","title":"两种方式构造数独"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); in.nextToken(); int n = (int) in.nval; long[] a = new long[n]; for (int i = 0; i \u0026lt; n; i++) { in.nextToken(); a[i] = (long) in.nval; } long maxU = Long.MIN_VALUE; long minU = Long.MAX_VALUE; long maxV = Long.MIN_VALUE; long minV = Long.MAX_VALUE; for (long i = 0; i \u0026lt; n; i++) { // 计算当前位置的索引（从1开始，而非0） long idx = i + 1; long val = a[(int) i]; // 计算u值：位置索引的平方 + 数值的平方 long u = idx * idx + val * val; // 计算v值：位置索引的平方 - 数值的平方 long v = idx * idx - val * val; maxU = Math.max(maxU, u); minU = Math.min(minU, u); maxV = Math.max(maxV, v); minV = Math.min(minV, v); } long dist = Math.max(maxU - minU, maxV - minV); out.println(dist); out.flush(); out.close(); br.close(); } ","date":"2026-02-08T23:57:10Z","permalink":"https://iamxurulin.github.io/p/%E6%9B%BC%E5%93%88%E9%A1%BF%E8%B7%9D%E7%A6%BBbishi25-%E6%9C%80%E5%A4%A7-fst-%E8%B7%9D%E7%A6%BB/","title":"【曼哈顿距离】BISHI25 最大 FST 距离"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); in.nextToken(); int n = (int) in.nval; // 创建HashMap：键=差值diff（Integer），值=该diff出现的次数（Integer） Map\u0026lt;Integer, Integer\u0026gt; hMap = new HashMap\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; n; i++) { in.nextToken(); int a = (int) in.nval; // diff = 前整数 - 它的位置索引 int diff = a - i; hMap.put(diff, hMap.getOrDefault(diff, 0) + 1); } long total = 0; // 遍历Map中所有diff的出现次数 for (int count : hMap.values()) { // 只有出现次数\u0026gt;1的diff，才能组成数对（至少2个元素才能配对） if (count \u0026gt; 1) { // 相同 diff 的数，两两之间都满足题目要求的条件， // 所以统计每个 diff 的出现次数，再算组合数就是答案。 total += (long) count * (count - 1) / 2; } } out.println(total); out.flush(); out.close(); br.close(); } 小贴士 $a_j -a_i = j-i$➡️$a_j - j= a_i-i$\n","date":"2026-02-08T22:46:33Z","permalink":"https://iamxurulin.github.io/p/bishi24-%E8%B0%90%E8%B7%9D%E4%B8%8B%E6%A0%87%E5%AF%B9/","title":"BISHI24 谐距下标对"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); String str = br.readLine(); String[] wordStrings = str.split(\u0026#34;\\\\s+\u0026#34;); Map\u0026lt;String, Integer\u0026gt; hMap = new HashMap\u0026lt;\u0026gt;(); for (String word : wordStrings) { hMap.put(word, hMap.getOrDefault(word, 0) + 1); } // Map.Entry\u0026lt;String, Integer\u0026gt;：表示Map中的一个键值对（包含key和value） List\u0026lt;Map.Entry\u0026lt;String, Integer\u0026gt;\u0026gt; keywords = new ArrayList\u0026lt;\u0026gt;(); for (Map.Entry\u0026lt;String, Integer\u0026gt; entry : hMap.entrySet()) { // 只保留出现次数≥3的单词 if (entry.getValue() \u0026gt;= 3) { keywords.add(entry); } } keywords.sort((o1, o2) -\u0026gt; { if (!o1.getValue().equals(o2.getValue()) ) { return o2.getValue() - o1.getValue();// 降序 } else { return o1.getKey().compareTo(o2.getKey());// 升序 } }); for(Map.Entry\u0026lt;String,Integer\u0026gt; entry:keywords){ out.println(entry.getKey()); } out.flush(); out.close(); br.close(); } 小贴士 hMap.entrySet()返回一个包含所有 Entry 的集合（Set\u0026lt;Map.Entry\u0026gt;）；\nEntry 是 Map 中键值对的最小封装单元，一个 Entry 对象 = 一个 key + 一个对应的 value。\n","date":"2026-02-08T21:42:22Z","permalink":"https://iamxurulin.github.io/p/bishi23-%E5%B0%8F%E7%BA%A2%E4%B9%A6%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/","title":"BISHI23 小红书推荐系统"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); in.nextToken(); int n = (int) in.nval; in.nextToken(); int m = (int) in.nval; List\u0026lt;int[]\u0026gt; a = new ArrayList\u0026lt;int[]\u0026gt;(); for (int i = 0; i \u0026lt; n; i++) { in.nextToken(); int x = (int) in.nval; in.nextToken(); int y = (int) in.nval; a.add(new int[] { x, y }); } int t = (int) (1.5 * m); a.sort((o1, o2) -\u0026gt; { if (o1[1] != o2[1]) { return o2[1] - o1[1];//降序 } else { return o1[0] - o2[0];//升序 } }); List\u0026lt;int[]\u0026gt; b = new ArrayList\u0026lt;int[]\u0026gt;(); // 存储符合条件的元素 int line = a.get(t - 1)[1]; // 取排序后第t个元素的y值（索引t-1，因为数组从0开始） for(int i=0;i\u0026lt;a.size();i++){ if(a.get(i)[1]\u0026lt;line){ // 数组已降序，遇到y\u0026lt;line直接终止遍历 break; } b.add(a.get(i)); // y≥line，加入结果集 } out.println(line+\u0026#34; \u0026#34;+b.size()); for(int i=0;i\u0026lt;b.size();i++){ out.println(b.get(i)[0]+\u0026#34; \u0026#34;+b.get(i)[1]); } out.flush(); out.close(); br.close(); } ","date":"2026-02-08T00:29:26Z","permalink":"https://iamxurulin.github.io/p/bishi22-%E5%88%86%E6%95%B0%E7%BA%BF%E5%88%92%E5%AE%9A/","title":"BISHI22 分数线划定"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); in.nextToken(); int a = (int) in.nval; in.nextToken(); int b = (int) in.nval; int start = a / 10000; int end = b / 10000; int count = 0; for (int y = start; y \u0026lt;= end; y++) { // 年份转字符串（如2001→\u0026#34;2001\u0026#34;） String year = Integer.toString(y); // 反转年份得到MMDD String revYear = new StringBuilder(year).reverse().toString(); // 2001→\u0026#34;1002\u0026#34; // 拆分反转后的字符串为月份、日期（\u0026#34;1002\u0026#34;→month=10，day=02） int month = Integer.parseInt(revYear.substring(0, 2)); int day = Integer.parseInt(revYear.substring(2, 4)); // 拼接成8位日期 int date = y * 10000 + month * 100 + day; // 日期在[a,b]区间内 并且 日期合法 if (date \u0026gt;= a \u0026amp;\u0026amp; date \u0026lt;= b) { if (isValidDate(y, month, day)) { count++; // 符合条件则计数+1 } } } out.println(count); out.flush(); out.close(); br.close(); } private static boolean isValidDate(int y, int month, int day) { if (month \u0026lt; 1 || month \u0026gt; 12 || day \u0026lt; 1) { return false; } int[] monthDay = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; if (isLeap(y)) { monthDay[2] = 29; } return day \u0026lt;= monthDay[month]; } private static boolean isLeap(int y) { return (y % 4 == 0 \u0026amp;\u0026amp; y % 100 != 0) || (y % 400 == 0); } ","date":"2026-02-07T23:14:50Z","permalink":"https://iamxurulin.github.io/p/bishi20-%E5%9B%9E%E6%96%87%E6%97%A5%E6%9C%9F/","title":"BISHI20 回文日期"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer in = new StringTokenizer(br.readLine()); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); String str = in.nextToken(); char[] s = str.toCharArray(); printScore(11, s, out); out.println(); printScore(21, s, out); out.flush(); out.close(); br.close(); } private static void printScore(int score, char[] s, PrintWriter out) { int w = 0; int l = 0; // 遍历每一个得分字符（W/L） for (int i = 0; i \u0026lt; s.length; i++) { // 统计得分：W则w+1，否则（L）l+1 if (s[i] == \u0026#39;W\u0026#39;) { w++; } else { l++; } // 判断是否赢下本局： // 条件1：分差≥2 // 条件2：任意一方达到目标分数（w≥score 或 l≥score） if (Math.abs(w - l) \u0026gt;= 2 \u0026amp;\u0026amp; (w \u0026gt;= score || l \u0026gt;= score)) { out.println(w + \u0026#34;:\u0026#34; + l); // 输出本局最终比分 w = 0; // 重置得分，开始下一局 l = 0; } } // 遍历完所有字符后，输出最后未完成局的剩余比分 out.println(w + \u0026#34;:\u0026#34; + l); } ","date":"2026-02-07T21:28:21Z","permalink":"https://iamxurulin.github.io/p/bishi19-%E4%B9%92%E4%B9%93%E7%90%83/","title":"BISHI19 乒乓球"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); in.nextToken(); int n = (int) in.nval; int[] a = new int[n + 1]; for (int i = n; i \u0026gt;= 0; i--) { in.nextToken(); a[i] = (int) in.nval; } for (int i = n; i \u0026gt;= 0; i--) { // 从最高次到0次遍历，按数学顺序输出 // 跳过系数为0的项（比如x²项系数为0，就不输出这一项） if (a[i] == 0) { continue; } // 处理符号 if (i == n) { // 最高次项 if (a[i] \u0026lt; 0) { // 最高次项为负，输出\u0026#34;-\u0026#34; out.print(\u0026#34;-\u0026#34;); } } else { // 非最高次项 if (a[i] \u0026lt; 0) { // 负数输出\u0026#34;-\u0026#34; out.print(\u0026#34;-\u0026#34;); } else { // 正数必须输出\u0026#34;+\u0026#34; out.print(\u0026#34;+\u0026#34;); } } // 处理系数 int val = Math.abs(a[i]); // 系数绝对值≠1 或 是常数项（i=0），才输出数字；否则不输出 if (val != 1 || i == 0) { out.print(val); } // 处理x的次数格式 if (i == 1) { // 一次项：输出\u0026#34;x\u0026#34; out.print(\u0026#34;x\u0026#34;); } else if (i \u0026gt; 1) { // 高于1次项：输出\u0026#34;x^次数\u0026#34; out.print(\u0026#34;x^\u0026#34; + i); } } out.flush(); out.close(); br.close(); } ","date":"2026-02-07T20:50:20Z","permalink":"https://iamxurulin.github.io/p/bishi18-%E5%A4%9A%E9%A1%B9%E5%BC%8F%E8%BE%93%E5%87%BA/","title":"BISHI18 多项式输出"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer in = new StringTokenizer(br.readLine()); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int t = Integer.parseInt(in.nextToken()); for (int i = 0; i \u0026lt; t; i++) { char[][] board = new char[3][3]; for (int j = 0; j \u0026lt; 3; j++) { String s = br.readLine().trim(); board[j] = s.toCharArray(); } boolean redEat = hasEat(board, \u0026#39;*\u0026#39;); boolean purpleEat = hasEat(board, \u0026#39;o\u0026#39;); if (redEat \u0026amp;\u0026amp; !purpleEat) { out.println(\u0026#34;yukari\u0026#34;); } else if (!redEat \u0026amp;\u0026amp; purpleEat) { out.println(\u0026#34;kou\u0026#34;); } else { out.println(\u0026#34;draw\u0026#34;); } } out.flush(); out.close(); br.close(); } private static boolean hasEat(char[][] board, char piece) { char opponent = (piece == \u0026#39;*\u0026#39;) ? \u0026#39;o\u0026#39; : \u0026#39;*\u0026#39;; //遍历 3 行（行索引 0-2）， // 仅检查 “该行中间列（索引 1）是己方棋子， // 左右列（索引 0、2）全是对方棋子” for (int row = 0; row \u0026lt; 3; row++) { if (board[row][1] == piece \u0026amp;\u0026amp; board[row][0] == opponent \u0026amp;\u0026amp; board[row][2] == opponent) { return true; } } //遍历 3 列（列索引 0-2）， // 仅检查 “该列中间行（索引 1）是己方棋子， // 上下行（索引 0、2）全是对方棋子” for (int col = 0; col \u0026lt; 3; col++) { if (board[1][col] == piece \u0026amp;\u0026amp; board[0][col] == opponent \u0026amp;\u0026amp; board[2][col] == opponent) { return true; } } return false; } 小贴士 逐行读取 3 行字符串时需要用br.readLine().trim()，可以避免StringTokenizer的分割符问题，其实这题用in.nextToken()还过不了。\n","date":"2026-02-06T23:20:05Z","permalink":"https://iamxurulin.github.io/p/bishi15%E5%B0%8F%E7%BA%A2%E7%9A%84%E5%A4%B9%E5%90%83%E6%A3%8B/","title":"【BISHI15】小红的夹吃棋"},{"content":"\n问题分析 由于每个数字的修改规则是仅x²\u0026lt;10时可改，并且只有2和3的修改会改变“各位和的模9值”，其他数字修改后模9值是不变的。\n假设初始各位和为sum，模9得rest = sum %9；\n如果rest=0，直接返回true；\n否则，需要通过修改k个2和m个3，让增量总和k*2 + m*6和 (9 - rest) %9相等。\n这样一来，问题就转化为判断是否存在k（≤count2）、m（≤count3）使得(k*2 + m*6) %9 == target。\n另外，\n由于2*9=18,所以改9个2和改0个2的效果是一样的，因此k的取值最多为min(count2,8)；\n由于6*3=18，所以改3个3和改0个3的效果也是一样的，因此m的取值最多为min(count3,2)；\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.util.StringTokenizer; public class Main { public static boolean isGoodNum(String s){ long sum = 0; int count2= 0; int count3=0; for(char c:s.toCharArray()){ int num = c-\u0026#39;0\u0026#39;; sum += num; if(num == 2){ count2++; }else if(num ==3){ count3++; } } int rest = (int)(sum%9); if(rest==0){ return true; } int target = (9-rest)%9; for(int i=0;i\u0026lt;=Math.min(count2,8);i++){ for(int j=0;j\u0026lt;=Math.min(count3,2);j++){ if((i*2+j*6)%9==target){ return true; } } } return false; } public static void main(String[] args) throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer in = new StringTokenizer(br.readLine()); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int t = Integer.parseInt(in.nextToken()); for(int i=0;i\u0026lt;t;i++){ in = new StringTokenizer(br.readLine()); String s = in.nextToken(); if(isGoodNum(s)){ out.println(\u0026#34;YES\u0026#34;); }else{ out.println(\u0026#34;NO\u0026#34;); } } out.flush(); out.close(); br.close(); } } ","date":"2026-02-06T21:28:19Z","permalink":"https://iamxurulin.github.io/p/bishi13-%E4%B9%9D%E5%80%8D%E5%B9%B3%E6%96%B9%E6%95%B0/","title":"BISHI13 九倍平方数"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public static void main(String[] args)throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer in = new StringTokenizer(br.readLine()); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int t = Integer.parseInt(in.nextToken()); for(int i=0;i\u0026lt;t;i++){ in = new StringTokenizer(br.readLine()); int n = Integer.parseInt(in.nextToken()); int[] nums = new int[n]; int count = 0; in = new StringTokenizer(br.readLine()); for(int j=0;j\u0026lt;n;j++){ nums[j]=Integer.parseInt(in.nextToken()); count+=nums[j]; } if(count%n!=0){ out.println(\u0026#34;NO\u0026#34;); }else{ /** * 问题转化为：能否通过题目允许的操作，将所有偏差值变为0。 */ for(int j=0;j\u0026lt;n;j++){ nums[j]-=(count/n); } /** * 将j-1位置的所有偏差值，全部累加到j+1位置，然后将j-1置为 0； * 说人话就是模拟能量从左向右转移，清空左侧方碑的偏差。 */ for(int j =1;j\u0026lt;n-1;j++){ nums[j+1]+=nums[j-1]; nums[j-1]=0; } boolean flag = true; for(int j=0;j\u0026lt;n;j++){ if(nums[j]!=0){ flag=false; break; } } out.println(flag?\u0026#34;YES\u0026#34;:\u0026#34;NO\u0026#34;); } } out.flush(); out.close(); br.close(); } ","date":"2026-02-06T00:32:53Z","permalink":"https://iamxurulin.github.io/p/bishi12%E5%85%83%E7%B4%A0%E6%96%B9%E7%A2%91/","title":"【BISHI12】元素方碑"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static void main(String[] args)throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer in = new StringTokenizer(br.readLine()); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); long x = Long.parseLong(in.nextToken()); long y = Long.parseLong(in.nextToken()); if(x==y){ out.println(0); }else if(y==0){ // 第二个数为0，1步操作即可满足条件 out.println(1); }else if(x==0){ // 第一个数为0，2步操作即可满足条件 out.println(2); }else if(x+y==0){ // 两数互为相反数（y = -x），3步操作满足条件 out.println(3); }else{ out.println(-1); } out.flush(); out.close(); br.close(); } ","date":"2026-02-05T23:36:21Z","permalink":"https://iamxurulin.github.io/p/bishi11%E5%8F%98%E5%B9%BB%E8%8E%AB%E6%B5%8B/","title":"【BISHI11】变幻莫测"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public static void main(String[] args)throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); StringTokenizer in = new StringTokenizer(br.readLine()); String s= in.nextToken(); in = new StringTokenizer(br.readLine()); String t= in.nextToken(); int min = Integer.MAX_VALUE; for(int i=0;i\u0026lt;=t.length()-s.length();i++){ int count = 0; //截取t中从i开始、长度为s的连续子串x String x = t.substring(i,i+s.length()); for(int j=0;j\u0026lt;s.length();j++){ //26-差值：计算环形绕行的差值（26 个小写字母首尾相连） count+=Math.min(Math.abs(x.charAt(j)-s.charAt(j)), 26-Math.abs(x.charAt(j)-s.charAt(j))); } min=Math.min(min,count); } out.println(min); out.flush(); out.close(); br.close(); } ","date":"2026-02-05T22:51:14Z","permalink":"https://iamxurulin.github.io/p/%E5%B0%8F%E7%BA%A2%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%BF%AE%E6%94%B9/","title":"小红的字符串修改"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int[] v = new int[3]; int[] a = new int[3]; StringTokenizer in = new StringTokenizer(br.readLine()); for (int i = 0; i \u0026lt; 3; i++) { v[i] = Integer.parseInt(in.nextToken()); } //读取第二行 in= new StringTokenizer(br.readLine()); for (int i = 0; i \u0026lt; 3; i++) { a[i] = Integer.parseInt(in.nextToken()); } Arrays.sort(v); Arrays.sort(a); if (a[2] \u0026gt; v[1] \u0026amp;\u0026amp; a[1] \u0026gt; v[0]) { out.println(\u0026#34;Yes\u0026#34;); } else { out.println(\u0026#34;No\u0026#34;); } out.flush(); out.close(); br.close(); } ","date":"2026-02-05T22:02:59Z","permalink":"https://iamxurulin.github.io/p/bishi9%E7%94%B0%E5%BF%8C%E8%B5%9B%E9%A9%AC/","title":"【BISHI9】田忌赛马"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer in = new StringTokenizer(br.readLine()); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); HashMap\u0026lt;Long, Long\u0026gt; hmap = new HashMap\u0026lt;\u0026gt;(); long total = 0; int n = Integer.parseInt(in.nextToken()); for (int i = 1; i \u0026lt;= n; i++) { in = new StringTokenizer(br.readLine()); long x = Long.parseUnsignedLong(in.nextToken()); long y = Long.parseUnsignedLong(in.nextToken()); long ans = hmap.getOrDefault(x, 0L); total += (long) i * ans; hmap.put(x, y); } out.println(Long.toUnsignedString(total)); out.flush(); out.close(); br.close(); } 小贴士 Long.parseUnsignedLong()专门解析 64 位无符号整数。 Long.toString()是把最高位当作符号位，输出带正负的有符号数,而Long.toUnsignedString()不会修改内存中的数据，只是换一种规则解读二进制，把所有位都当作数值位，直接解析为无符号十进制数，和题目要求的 $\\text{mod}~2^{64}$结果完全一致。 ","date":"2026-02-05T20:38:24Z","permalink":"https://iamxurulin.github.io/p/%E5%A4%A7%E6%95%B4%E6%95%B0%E5%93%88%E5%B8%8C/","title":"大整数哈希"},{"content":"在 IDEA 中写完了新增各种工具类的代码，完成了 Commit 和 Push，代码已经到了 GitHub远程仓库。\n继续写着下一个功能的代码，突然，我发现刚才的Commit Message少写了一个已经完成的功能实现。\n此时，我的工作区里已经有正在修改但暂时还不想提交的文件：\n于是，我使用了 IDEA 的修正提交功能，补全了信息，重新点击了 Commit：\n但是，当我再次点击 Push 时，出现了如下报错：\n1 2 3 4 error: failed to push some refs to \u0026#39;github.com:xxx/xxx.git\u0026#39; !\trefs/heads/main:refs/heads/main\t[rejected] (non-fast-forward) hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart. 这个错误说人话就是：推送到远程被拒绝了！并且Git 提示你需要先 Pull。\n原因分析 远程仓库保存着修改前的那次提交，我们把它记为 Commit A；\n本地仓库在我使用修正功能时，Git 并没有直接修改 Commit A，而是把 Commit A 扔掉了，生成了一个全新的 Commit A’，这个新的commit有着新的 Hash 值，新的备注。\n当我点击 Push 时，远程仓库发现现在推送的 Commit A’ 和已有的 Commit A 对不上，为了防止覆盖代码，Git 默认拦截了这次推送。\n解决方案 本来我想使用 IDEA 的图形界面，但是，强制推送的按钮居然是灰色的，选不上。\n所有，我使用命令行，在 IDEA 下方的 Terminal 中输入以下命令：\n1 git push -f origin main 之后，就推送成功了。\n小贴士 这样操作后，GitHub 上并不会多出一条记录，GitHub 上的记录会原地刷新。旧的提交（也就是少信息的那个）会被彻底删除，取而代之的是修改后的新提交。历史记录看起来非常干净，就像从来没有犯过错一样。 工作区里那个已修改但没勾选的文件不会受到任何影响，可以在下一次准备好时再提交。 ","date":"2026-02-05T14:58:05Z","permalink":"https://iamxurulin.github.io/p/%E5%B7%B2%E7%BB%8F-push-%E5%88%B0%E8%BF%9C%E7%A8%8B%E7%9A%84%E6%8F%90%E4%BA%A4%E5%A6%82%E4%BD%95%E4%BF%AE%E6%94%B9-commit-%E4%BF%A1%E6%81%AF/","title":"已经 Push 到远程的提交，如何修改 Commit 信息？"},{"content":"在开发 Spring Boot +LangGraph4J项目时，打算利用阿里云百炼的文生图模型生成Logo图片，在引入了阿里云百炼 SDK（dashscope-sdk-java）后，执行单元测试（LogoGeneratorToolTest）时，项目直接启动失败，控制台打印大量报错日志:\n1 2 3 4 5 SLF4J(W): Class path contains multiple SLF4J providers. SLF4J(W): Found provider [org.slf4j.simple.SimpleServiceProvider@206a70ef] SLF4J(W): Found provider [ch.qos.logback.classic.spi.LogbackServiceProvider@292b08d6] ... Caused by: java.lang.IllegalStateException: LoggerFactory is not a Logback LoggerContext but Logback is on the classpath. Either remove Logback or the competing implementation (class org.slf4j.simple.SimpleLoggerFactory loaded from .../slf4j-simple-2.0.17.jar) 原因分析 SLF4J（Simple Logging Facade for Java）是日志门面接口，本身不实现日志打印功能，需要绑定一个具体的日志实现组件（如Logback、Log4j、SLF4J-Simple等）。\n其中，Logback是由spring-boot-starter-logging自动引入，Spring Boot官方推荐的日志组件，支持日志配置、文件滚动、异步打印等企业级特性。\n而SLF4J-Simple是由阿里通义SDK（dashscope-sdk-java）依赖传递引入，是一个轻量但功能简单的日志实现。\n由于在这个项目类路径下同时存在两个SLF4J的实现组件，违反了“一个日志门面+一个实现”的规范，导致Spring容器启动时无法确定使用哪个日志工厂，最终加载应用上下文失败。\n解决方案 保留Logback，排除冲突的SLF4J-Simple依赖，修改pom.xml，排除冲突依赖，通过标签排除其中的slf4j-simple依赖，具体配置如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;!-- 阿里通义SDK依赖 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;dashscope-sdk-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.21.1\u0026lt;/version\u0026gt; \u0026lt;!-- 排除冲突的slf4j-simple依赖 --\u0026gt; \u0026lt;exclusions\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;groupId\u0026gt;org.slf4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;slf4j-simple\u0026lt;/artifactId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;/exclusions\u0026gt; \u0026lt;/dependency\u0026gt; 接下来，打开Maven面板➡️ 右键项目➡️选择“Reload Project”，等待依赖刷新完成，重新运行之前失败的单元测试，日志冲突问题解决。\n","date":"2026-02-05T12:22:13Z","permalink":"https://iamxurulin.github.io/p/spring-boot%E6%B5%8B%E8%AF%95%E5%90%AF%E5%8A%A8%E5%A4%B1%E8%B4%A5slf4j%E6%97%A5%E5%BF%97%E5%A4%9A%E5%AE%9E%E7%8E%B0%E5%86%B2%E7%AA%81%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/","title":"Spring Boot测试启动失败：SLF4J日志多实现冲突解决方案"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); Deque\u0026lt;Integer\u0026gt; queue = new ArrayDeque\u0026lt;\u0026gt;(); while (in.nextToken() != StreamTokenizer.TT_EOF) { int n = (int) in.nval; for (int i = 0; i \u0026lt; n; i++) { in.nextToken(); int a = (int) in.nval; switch (a) { case 1: in.nextToken(); queue.add((int) in.nval); break; case 2: if (queue.isEmpty()) { out.println(\u0026#34;ERR_CANNOT_POP\u0026#34;); } else { queue.pollFirst(); } break; case 3: if (queue.isEmpty()) { out.println(\u0026#34;ERR_CANNOT_QUERY\u0026#34;); } else { out.println(queue.peekFirst()); } break; case 4: out.println(queue.size()); break; default: break; } } } out.flush(); out.close(); } ","date":"2026-02-04T19:46:59Z","permalink":"https://iamxurulin.github.io/p/acm%E6%A8%A1%E5%BC%8F%E9%98%9F%E5%88%97%E6%93%8D%E4%BD%9C/","title":"【ACM模式】队列操作"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public static void main(String[] args)throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); Deque\u0026lt;Integer\u0026gt; stack = new ArrayDeque\u0026lt;\u0026gt;(); in.nextToken(); int n = (int)in.nval; for(int i=0;i\u0026lt;n;i++){ in.nextToken(); String str = (String)in.sval; switch (str) { case \u0026#34;push\u0026#34;: in.nextToken(); stack.push((int)in.nval); break; case \u0026#34;pop\u0026#34;: if(stack.isEmpty()){ out.println(\u0026#34;Empty\u0026#34;); }else{ stack.pop(); } break; case \u0026#34;query\u0026#34;: if(stack.isEmpty()){ out.println(\u0026#34;Empty\u0026#34;); }else{ out.println(stack.peek()); } break; case \u0026#34;size\u0026#34;: out.println(stack.size()); break; default: break; } } out.flush(); out.close(); } 小贴士 官方文档明确标注：\nDeque接口及其实现类（如ArrayDeque）应优先于Stack类使用。\n","date":"2026-02-04T18:02:20Z","permalink":"https://iamxurulin.github.io/p/acm%E6%A8%A1%E5%BC%8F%E6%A0%88%E7%9A%84%E6%93%8D%E4%BD%9C/","title":"【ACM模式】栈的操作"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.StreamTokenizer; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class BISHI1 { public static int q; public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); List\u0026lt;Integer\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); while (in.nextToken() != StreamTokenizer.TT_EOF) { q = (int) in.nval; for (int i = 0; i \u0026lt; q; i++) { in.nextToken(); int a = (int) in.nval; switch (a) { case 1: in.nextToken(); list.add((int) in.nval); break; case 2: list.remove(list.size() - 1); break; case 3: in.nextToken(); out.println(list.get((int) in.nval)); break; case 4: in.nextToken(); int ii = (int) in.nval; in.nextToken(); int iii = (int) in.nval; list.add(ii+1,iii); break; case 5: Collections.sort(list); break; case 6: Collections.sort(list,(o1,o2)-\u0026gt;(o2-o1)); break; case 7: out.println(list.size()); break; case 8: list.forEach(y-\u0026gt;out.print(y+\u0026#34; \u0026#34;)); out.println(); break; default: break; } } } out.flush(); out.close(); } } ","date":"2026-02-04T00:15:04Z","permalink":"https://iamxurulin.github.io/p/acm%E6%A8%A1%E5%BC%8F%E5%BA%8F%E5%88%97%E6%93%8D%E4%BD%9C/","title":"【ACM模式】序列操作"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public int shipWithinDays(int[] weights, int days) { int left = 0; int sum = 0; for (int weight : weights) { left = Math.max(left, weight); sum += weight; } int right = sum; while (left \u0026lt; right) { int mid = left + ((right - left) \u0026gt;\u0026gt; 1); if (f(weights, mid) \u0026lt;= days) { // 满足天数要求：尝试更小的运载能力，收缩右边界 right = mid; } else { // 不满足要求：必须增大运载能力，收缩左边界 left = mid + 1; } } return left; } private int f(int[] weights, int capacity) { int days = 0; for (int i = 0; i \u0026lt; weights.length;) { int cap = capacity; while (i \u0026lt; weights.length) { // 装不下当前包裹，结束本批次运输 if (cap \u0026lt; weights[i]) { break; } else { cap -= weights[i]; } i++; } // 完成一趟运输，天数+1 days++; } return days; } ","date":"2026-02-03T22:23:39Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%88%86%E6%B3%95%E5%9C%A8-d-%E5%A4%A9%E5%86%85%E9%80%81%E8%BE%BE%E5%8C%85%E8%A3%B9%E7%9A%84%E8%83%BD%E5%8A%9B/","title":"【二分法】在 D 天内送达包裹的能力"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public int findKthNumber(int n, int k) { int cur = 1;// 从字典序第一个数字 1 开始 k--;// 转换为 0 索引 while (k \u0026gt; 0) { // 计算以cur为根的子树，包含的有效节点数量 int steps = getSteps(cur, n); if (steps \u0026lt;= k) { // 目标不在当前子树，跳过整棵子树，更新k和当前节点 k -= steps; cur++; } else { // 目标在子树中，进入子节点，k减1（跳过当前节点） cur *= 10; k--; } } return cur; } // 计算以cur为根节点的子树中，\u0026lt;=n的节点总数 private int getSteps(int cur, long n) { int steps = 0; long first = cur; // 当前层起始节点 long last = cur; // 当前层结束节点 while (first \u0026lt;= n) { // 累加当前层的节点数，防止last超出n steps += Math.min(last, n) - first + 1; // 进入下一层 first *= 10; last = last * 10 + 9; } return steps; } 小贴士 k-- 是为了将1-based的输入转换为0-based计数 first/last 必须用 long 类型，避免 int 乘法溢出； ","date":"2026-02-03T21:32:47Z","permalink":"https://iamxurulin.github.io/p/%E5%8D%81%E5%8F%89%E6%A0%91%E7%9A%84%E5%85%88%E5%BA%8F%E9%81%8D%E5%8E%86%E5%AD%97%E5%85%B8%E5%BA%8F%E7%9A%84%E7%AC%ACk%E5%B0%8F%E6%95%B0%E5%AD%97/","title":"【十叉树的先序遍历】字典序的第K小数字"},{"content":"在基于 LangChain4j + Spring Boot 开发AI代码生成平台的过程中，主要想实现Vue项目带工具调用的流式生成能力，比如自动写入项目文件、构建打包，同时通过响应式流向前端推送实时进度。\n但是，在前端调试时，出现了以下错误：\n原因分析 1. AI服务接口 1 2 @SystemMessage(fromResource = \u0026#34;prompt/codegen-vue-project-system-prompt.txt\u0026#34;) Flux\u0026lt;String\u0026gt; generateVueProjectCodeStream(@MemoryId long appId, @UserMessage String userMessage); 2. Facade层调度逻辑 1 2 3 4 case VUE_PROJECT -\u0026gt; { Flux codeStream = aiCodeGeneratorService.generateVueProjectCodeStream(appId, userMessage); yield processCodeStream(codeStream, CodeGenTypeEnum.VUE_PROJECT, appId); } 接口调用直接返回业务异常，前端无任何流式输出，日志无有效业务堆栈，仅提示：抱歉，生成过程中出现了错误，请重试。\n主要原因是：\nTokenStream 是LangChain4j专为AI流式响应 + 工具调用设计的API，内置 onToolRequest/onToolExecuted 等完整回调事件，可无缝衔接工具执行流程； Flux\u0026lt;String\u0026gt; 是通用响应式流组件，无AI场景专属能力，无法接收、处理工具调用的回调事件。 解决方法 将接口和调度层作如下修改：\n1 2 3 4 5 6 7 8 9 // 1. AI接口原生返回值类型 @SystemMessage(fromResource = \u0026#34;prompt/codegen-vue-project-system-prompt.txt\u0026#34;) TokenStream generateVueProjectCodeStream(@MemoryId long appId, @UserMessage String userMessage); // 2. Facade层调度逻辑 case VUE_PROJECT -\u0026gt; { TokenStream tokenStream = aiCodeGeneratorService.generateVueProjectCodeStream(appId, userMessage); yield processTokenStream(tokenStream); } 接下来，就恢复正常了。\n小贴士 在集成工具调用的LangChain4j AI场景中，TokenStream 是官方推荐且唯一能保证全功能正常运行的流式类型，Flux 仅适用于无AI业务逻辑的通用流式场景。\n在集成第三方框架时，优先使用框架原生提供的组件和API，是保证系统稳定性的最优解。\n","date":"2026-02-03T12:35:47Z","permalink":"https://iamxurulin.github.io/p/langchain4j-%E8%B8%A9%E5%9D%91%E5%AE%9E%E5%BD%95ai-%E5%B7%A5%E5%85%B7%E8%B0%83%E7%94%A8%E6%B5%81%E5%BC%8F%E5%BC%80%E5%8F%91tokenstream-%E6%89%8D%E6%98%AF%E6%AD%A3%E7%A1%AE%E9%80%89%E6%8B%A9/","title":"LangChain4j 踩坑实录：AI 工具调用流式开发，TokenStream 才是正确选择"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public List\u0026lt;String\u0026gt; binaryTreePaths(TreeNode root) { List\u0026lt;String\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); if (root == null) { return ans; } StringBuilder sb = new StringBuilder(); backtrack(root, sb, ans); return ans; } private void backtrack(TreeNode root, StringBuilder sb, List\u0026lt;String\u0026gt; ans) { if (root == null) { return; } // 记录当前拼接前的长度，用于回溯撤销 int len = sb.length(); sb.append(root.val); // 到达叶子节点，添加完整路径 if (root.left == null \u0026amp;\u0026amp; root.right == null) { ans.add(sb.toString()); } else { sb.append(\u0026#34;-\u0026gt;\u0026#34;); backtrack(root.left, sb, ans); backtrack(root.right, sb, ans); } // 回溯撤销，恢复StringBuilder到拼接前的状态 sb.setLength(len); } ","date":"2026-02-02T23:27:38Z","permalink":"https://iamxurulin.github.io/p/%E5%9B%9E%E6%BA%AF%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%89%80%E6%9C%89%E8%B7%AF%E5%BE%84/","title":"【回溯】二叉树的所有路径"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public String multiply(String num1, String num2) { if (\u0026#34;0\u0026#34;.equals(num1) || \u0026#34;0\u0026#34;.equals(num2)) { return \u0026#34;0\u0026#34;; } int len1 = num1.length(); int len2 = num2.length(); int[] res = new int[len1 + len2]; // 从后往前遍历 for (int i = len1 - 1; i \u0026gt;= 0; i--) { int n1 = num1.charAt(i) - \u0026#39;0\u0026#39;; for (int j = len2 - 1; j \u0026gt;= 0; j--) { int n2 = num2.charAt(j) - \u0026#39;0\u0026#39;; int sum = n1 * n2 + res[i + j + 1]; // 加上原来的低位 res[i + j + 1] = sum % 10; res[i + j] += sum / 10; } } StringBuilder sb = new StringBuilder(); for (int num : res) { if (!(sb.length() == 0 \u0026amp;\u0026amp; num == 0)) { sb.append(num); } } return sb.length() == 0 ? \u0026#34;0\u0026#34; : sb.toString(); } ","date":"2026-02-02T22:40:17Z","permalink":"https://iamxurulin.github.io/p/%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9B%B8%E4%B9%98/","title":"字符串相乘"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public int numPairsDivisibleBy60(int[] time) { // 创建长度为60的数组，下标对应余数0~59，存储对应余数出现的次数 int[] arr = new int[60]; int ans = 0; for (int t : time) { // 计算当前时长对60取余，将数值范围压缩到 0~59 t %= 60; //查找能和当前余数t配对的余数的出现次数，累加到结果中 ans += arr[(60 - t) % 60]; // 将当前余数的计数+1，存入数组，供后续元素配对使用 arr[t]++; } return ans; } ","date":"2026-02-02T21:39:04Z","permalink":"https://iamxurulin.github.io/p/%E6%80%BB%E6%8C%81%E7%BB%AD%E6%97%B6%E9%97%B4%E5%8F%AF%E8%A2%AB-60-%E6%95%B4%E9%99%A4%E7%9A%84%E6%AD%8C%E6%9B%B2/","title":"总持续时间可被 60 整除的歌曲"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public int maxDistance(List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; arrays) { // 初始化最小值为第一个数组的第一个元素（升序数组的最小值） int min = arrays.get(0).get(0); // 初始化最大值为第一个数组的最后一个元素（升序数组的最大值） int max = arrays.get(0).get(arrays.get(0).size() - 1); int res = 0; for (int i = 1; i \u0026lt; arrays.size(); i++) { // 计算当前数组的最大值 与 之前所有数组最小值 的差，更新最大距离 res = Math.max(res, Math.abs(arrays.get(i).get(arrays.get(i).size() - 1) - min)); // 计算之前所有数组最大值 与 当前数组最小值 的差，更新最大距离 res = Math.max(res, Math.abs(max - arrays.get(i).get(0))); // 更新全局最小值 min = Math.min(min, arrays.get(i).get(0)); // 更新全局最大值 max = Math.max(max, arrays.get(i).get(arrays.get(i).size() - 1)); } return res; } ","date":"2026-02-02T20:29:23Z","permalink":"https://iamxurulin.github.io/p/%E6%95%B0%E7%BB%84%E5%88%97%E8%A1%A8%E4%B8%AD%E7%9A%84%E6%9C%80%E5%A4%A7%E8%B7%9D%E7%A6%BB/","title":"数组列表中的最大距离"},{"content":"在使用 Spring Boot + LangChain4j 开发 AI 应用时，在 Service 工厂中注入如下两个Bean：\n1 2 3 4 5 @Resource private StreamingChatModel openAiStreamingChatModel; @Resource private StreamingChatModel reasoningStreamingChatModel; 一切看起来都很合理，但启动时就报如下错误❌：\n1 Bean named \u0026#39;openAiStreamingChatModel\u0026#39; is expected to be of type \u0026#39;dev.langchain4j.model.chat.StreamingChatModel\u0026#39; but was actually of type \u0026#39;dev.langchain4j.model.openai.OpenAiStreamingChatModel\u0026#39; 最诡异的是 OpenAiStreamingChatModel 明明是 StreamingChatModel 的子类，期望类型和实际类型“看起来完全兼容”，但还是报错！\n原因分析 这个错误的根源，其实不是代码逻辑的问题，而是 spring-boot-devtools 的热部署机制导致的类加载冲突。\nSpring Boot DevTools 为了实现快速重启，使用了两个类加载器。 一个是Base ClassLoader ，主要负责加载Spring Boot 框架、第三方 jar（比如 langchain4j-core.jar）；\n另一个是Restart ClassLoader，主要负责加载自己的项目代码（比如src/main/java）。\n默认情况下，DevTools 会把 所有非项目代码的 jar 放入 Base ClassLoader。\n但是！ LangChain4j 这类库，通过 Maven 引入，它其实是属于“第三方依赖”，理应由 Base ClassLoader 加载。\n然而，在某些版本或配置下，DevTools 可能错误地将部分 LangChain4j 类交给了 Restart ClassLoader。\n这样就造成同一个类，有两个身份，这样 JVM 认为这是两个完全无关的类。即使包名、类名、继承关系都对，也无法进行类型转换或赋值。\n解决方案 1.在项目的如下目录中创建文件：\n1 src/main/resources/META-INF/spring-devtools.properties 2.文件内容如下，主要目的是告诉 DevTools，这些 jar 属于基础类路径，统一用 Base ClassLoader 加载。\n1 2 restart.include.langchain4j=/langchain4j-.*\\.jar restart.include.openai=/openai-.*\\.jar 3.重新运行，问题解决。\n","date":"2026-02-02T15:26:29Z","permalink":"https://iamxurulin.github.io/p/spring-boot--langchain4j-%E6%8A%A5%E9%94%99bean-%E7%B1%BB%E5%9E%8B%E4%B8%8D%E5%8C%B9%E9%85%8D%E7%9A%84%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95/","title":"Spring Boot + LangChain4j 报错：Bean 类型不匹配的解决办法"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public int countSubstrings(String s) { if(s==null||s.length()==0){ return 0; } int ans = 0; for(int i=0;i\u0026lt;s.length();i++){ // 1. 以i为中心（奇数长度回文） ans += countPalindrome(s, i, i); // 2. 以i和i+1为中心（偶数长度回文） ans += countPalindrome(s, i, i+1); } return ans; } private int countPalindrome(String s,int left,int right) { int count = 0; while(left\u0026gt;=0\u0026amp;\u0026amp; right\u0026lt; s.length()\u0026amp;\u0026amp;s.charAt(left)==s.charAt(right)){ count++; left--; right++; } return count; } ","date":"2026-02-01T23:41:08Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%AD%E5%BF%83%E6%89%A9%E5%B1%95%E6%B3%95lcr_020_%E5%9B%9E%E6%96%87%E5%AD%90%E4%B8%B2/","title":"【中心扩展法】LCR_020_回文子串"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public boolean validPalindrome(String s) { int len = s.length(); int left = 0; int right = len-1; while(left\u0026lt;right){ if(s.charAt(left)==s.charAt(right)){ left++; right--; }else{ return valid(s,left+1,right)||valid(s, left, right-1); } } return true; } public boolean valid(String s,int left,int right) { while (left\u0026lt;right) { if(s.charAt(left)!=s.charAt(right)){ return false; } left++; right--; } return true; } ","date":"2026-02-01T22:49:20Z","permalink":"https://iamxurulin.github.io/p/lcr_019_%E9%AA%8C%E8%AF%81%E5%9B%9E%E6%96%87%E4%B8%B2ii/","title":"LCR_019_验证回文串II"},{"content":"在Spring Boot项目中引入Redis配置类时，出现以下警告提示： 原因分析 坦白说，这个警告 不是代码错误，并不会影响程序的运行。\nSpring Boot 提供了一个名为 spring-boot-configuration-processor 的工具。\n它的作用是在编译时扫描你的项目，找到所有带 @ConfigurationProperties 的类，并生成一个名为 spring-configuration-metadata.json 的元数据文件。 IDEA 正是读取这个文件，才能在 application.yml 中，当你输入 spring.data.redis...，给你提供智能提示。\n如果没有配置这个processor，IDEA 就只能给你弹个警告。\n但是，话又说回来，虽然程序能正常运行，这个警告看着灰常难受，并且失去了 Spring Boot 强大的配置提示功能。\n解决方案 1.在 pom.xml 文件中，添加以下依赖：\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-configuration-processor\u0026lt;/artifactId\u0026gt; \u0026lt;optional\u0026gt;true\u0026lt;/optional\u0026gt; \u0026lt;/dependency\u0026gt; 2.添加完后，点击 IDEA 右侧 Maven 面板的 \u0026ldquo;Reload All Maven Projects\u0026rdquo;（刷新按钮），确保 jar 包被真正下载。\n3.启用 IDEA 注解处理\n4.强制重新编译（Rebuild）\n点击 IDEA 顶部菜单栏的【Build】，选择【 Rebuild Project】。\n⚠️：一定要选【Rebuild】，不是【Build】。\n5.接下来点击【隐藏通知】就好了。 ","date":"2026-02-01T14:34:57Z","permalink":"https://iamxurulin.github.io/p/idea-%E6%8F%90%E7%A4%BA%E6%9C%AA%E9%85%8D%E7%BD%AEspringboot%E9%85%8D%E7%BD%AE%E6%B3%A8%E8%A7%A3%E5%A4%84%E7%90%86%E5%99%A8%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/","title":"IDEA 提示“未配置SpringBoot配置注解处理器“的解决方案"},{"content":"这几个词经常一起出现，有次面试的时候被问到了，还是有点懵的，本文就来理清一下。\n1. 内存（Memory） 本质上是硬件，比如电脑/服务器上的内存条，速度极快（纳秒级），缺点是断电会导致数据丢失。\n需要注意：程序运行时必须把数据加载到内存才能执行。\n2. 数据库（Database） 本质上是软件，用来持久化存储数据（断电不丢失），提供查询、修改、事务等能力，数据存在硬盘。\n3. MySQL 本质上是关系型数据库（ Relational Database Management System, RDBMS），数据存在硬盘，支持 SQL、事务、索引、复杂查询，速度比内存慢很多（毫秒级）。\n需要注意的是MySQL 是数据库的一种，但是数据库 ≠ MySQL。\n4. 缓存（Cache） 本质上其实是一种“加速思想或者机制”，不是软件也不是硬件，而是一种设计模式，主要用于把热点数据放在更快的地方，减少查询压力。\n5. Redis 本质上是基于内存的 NoSQL 数据库，数据主要存在内存，也可以持久化到硬盘，常被当作分布式缓存使用。\n需要注意的是：\nRedis 是一个数据库，Redis ≠ 缓存，只是它常被用来做缓存，但它功能远不止缓存，比如也可以做排行榜、限流、分布式锁等等。\n","date":"2026-02-01T13:02:50Z","permalink":"https://iamxurulin.github.io/p/redis%E5%86%85%E5%AD%98%E7%BC%93%E5%AD%98mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E8%BF%99%E4%BA%9B%E7%9A%84%E5%8C%BA%E5%88%AB%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88/","title":"Redis、内存、缓存、MySQL、数据库，这些的区别到底是什么"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public List\u0026lt;Integer\u0026gt; findAnagrams(String s, String p) { List\u0026lt;Integer\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); if (s.length() \u0026lt; p.length()) { return ans; } int[] arrP = new int[26]; // 统计p的字符出现次数 int[] arrS = new int[26]; // 统计s滑动窗口内的字符出现次数 for (int i = 0; i \u0026lt; p.length(); i++) { arrP[p.charAt(i) - \u0026#39;a\u0026#39;]++; // p的第i个字符对应数组下标，计数+1 arrS[s.charAt(i) - \u0026#39;a\u0026#39;]++; // 先统计s前p.length()个字符的计数 } // 用于比较两个数组的内容是否完全相等 if (Arrays.equals(arrP, arrS)) { ans.add(0); } // 初始窗口是[0, p.length()-1]，右边界从p.length()开始 int left = 0; int right = p.length(); while (right \u0026lt; s.length()) { // 右边界字符加入窗口：计数+1 arrS[s.charAt(right) - \u0026#39;a\u0026#39;]++; // 左边界字符移出窗口：计数-1 arrS[s.charAt(left) - \u0026#39;a\u0026#39;]--; // 窗口右移：左、右边界各+1 left++; right++; // 此时窗口起始下标是left，判断是否匹配 if (Arrays.equals(arrP, arrS)) { ans.add(left); } } return ans; } 小贴士 这道题思路和 【滑动窗口+字符计数数组】LCR_014_字符串的排列 基本一致，只不过有一些细节上的东西需要注意。\n比如字符串排列那道题的处理顺序是：\n更新计数 ➡️ 判断 ➡️ 移动边界\n而这道题是：\n更新计数 ➡️ 移动边界 ➡️ 判断\n","date":"2026-01-31T22:51:13Z","permalink":"https://iamxurulin.github.io/p/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3-%E8%AE%A1%E6%95%B0lcr015%E6%89%BE%E5%88%B0%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B8%AD%E6%89%80%E6%9C%89%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D/","title":"【滑动窗口+计数】LCR015找到字符串中所有字母异位词"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public boolean checkInclusion(String s1, String s2) { if (s1.length() \u0026gt; s2.length()) { return false; } int[] a = new int[26]; // 统计s1的字符出现次数 int[] b = new int[26]; // 统计s2滑动窗口内的字符出现次数 for (int i = 0; i \u0026lt; s1.length(); i++) { a[s1.charAt(i) - \u0026#39;a\u0026#39;]++; // s1的第i个字符对应数组下标（如\u0026#39;a\u0026#39;→0，\u0026#39;b\u0026#39;→1），计数+1 b[s2.charAt(i) - \u0026#39;a\u0026#39;]++; // 先统计s2前s1.length()个字符的计数 } //用于比较两个数组的内容是否完全相等 if (Arrays.equals(a, b)) { return true; } // 初始窗口是[0, s1.length()-1]，右边界从s1.length()开始 int left = 0; int right = s1.length(); while (right \u0026lt; s2.length()) { // 右边界字符加入窗口：计数+1 b[s2.charAt(right) - \u0026#39;a\u0026#39;]++; // 左边界字符移出窗口：计数-1 b[s2.charAt(left) - \u0026#39;a\u0026#39;]--; // 检查当前窗口计数是否匹配 if (Arrays.equals(a, b)) { return true; } // 窗口右移：左、右边界各+1 right++; left++; } return false; } ","date":"2026-01-31T22:27:27Z","permalink":"https://iamxurulin.github.io/p/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3-%E5%AD%97%E7%AC%A6%E8%AE%A1%E6%95%B0%E6%95%B0%E7%BB%84lcr_014_%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E6%8E%92%E5%88%97/","title":"【滑动窗口+字符计数数组】LCR_014_字符串的排列"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private int[][] preSum; public NumMatrix(int[][] matrix) { int m = matrix.length; int n = matrix[0].length; if(m==0||n==0){ return; } preSum = new int[m+1][n+1]; for(int i=1;i\u0026lt;=m;i++){ for(int j=1;j\u0026lt;=n;j++){ preSum[i][j]=preSum[i-1][j]+preSum[i][j-1]+matrix[i-1][j-1]-preSum[i-1][j-1]; } } } public int sumRegion(int row1, int col1, int row2, int col2) { return preSum[row2+1][col2+1]-preSum[row2+1][col1]-preSum[row1][col2+1]+preSum[row1][col1]; } 小贴士 预处理：preSum[i][j] = 上 + 左 + 当前元素 - 重复部分；\n查询：区域和 = 整体和 - 左侧和 - 上方和 + 重复和。\n","date":"2026-01-31T21:33:54Z","permalink":"https://iamxurulin.github.io/p/%E5%89%8D%E7%BC%80%E5%92%8Clcr_013_%E4%BA%8C%E7%BB%B4%E5%8C%BA%E5%9F%9F%E5%92%8C%E6%A3%80%E7%B4%A2-%E7%9F%A9%E9%98%B5%E4%B8%8D%E5%8F%AF%E5%8F%98/","title":"【前缀和】LCR_013_二维区域和检索-矩阵不可变"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public int pivotIndex(int[] nums) { int leftSum = 0; int rightSum = 0; // 遍历数组，把所有元素的和存入 rightSum， // 此时 rightSum 是 “整个数组的和” for (int num : nums) { rightSum += num; } for (int i = 0; i \u0026lt; nums.length; i++) { // 从总和中减去当前元素，rightSum 变为“下标i右侧所有元素的和” rightSum -= nums[i]; // 判断“右侧和”是否等于“左侧和”，相等则当前i就是中心下标 if (rightSum == leftSum) { return i; } // 将当前元素加入左侧和，为下一个下标的判断做准备 leftSum += nums[i]; } return -1; } ","date":"2026-01-31T20:28:31Z","permalink":"https://iamxurulin.github.io/p/%E6%80%BB%E5%92%8C%E6%8B%86%E5%88%86--%E5%8F%8C%E5%8F%98%E9%87%8F%E9%81%8D%E5%8E%86lcr_012_%E5%AF%BB%E6%89%BE%E6%95%B0%E7%BB%84%E7%9A%84%E4%B8%AD%E5%BF%83%E4%B8%8B%E6%A0%87/","title":"【总和拆分 + 双变量遍历】LCR_012_寻找数组的中心下标"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public int findMaxLength(int[] nums) { int ans = 0; int sum = 0; HashMap\u0026lt;Integer,Integer\u0026gt; hMap = new HashMap\u0026lt;\u0026gt;(); hMap.put(0, -1); for(int i=0;i\u0026lt;nums.length;i++){ sum+=(nums[i]==0?-1:1); if(hMap.containsKey(sum)){ ans=Math.max(ans, i-hMap.get(sum)); }else{ hMap.put(sum, i); } } return ans; } 小贴士 这道题关键在于把0转为-1，1保持1，将问题转化为“找和为0的最长子数组”。\n解释一下这行代码：\n1 map.put(0, -1); 假设从数组起始位置（下标 0）到下标 i 的前缀和为 0，说明 [0, i] 区间内 0 和 1 数量相等，此时子数组长度就是 i - (-1) = i + 1\n","date":"2026-01-31T19:56:13Z","permalink":"https://iamxurulin.github.io/p/%E5%89%8D%E7%BC%80%E5%92%8C-%E5%93%88%E5%B8%8Clcr_011_%E8%BF%9E%E7%BB%AD%E6%95%B0%E7%BB%84/","title":"【前缀和+哈希】LCR_011_连续数组"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public int subarraySum(int[] nums, int k) { // 哈希表：key=前缀和，value=该前缀和出现的次数 HashMap\u0026lt;Integer, Integer\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); int sum = 0; // 记录当前遍历到的前缀和 int ans = 0; // 初始化前缀和为0的情况出现了1次 map.put(0, 1); for (int i = 0; i \u0026lt; nums.length; i++) { sum += nums[i]; // 累加当前元素，得到当前前缀和sum // 查找是否存在sum - k，存在则累加次数到结果 if (map.containsKey(sum - k)) { ans += map.get(sum - k); } // 将当前前缀和存入哈希表：若已存在则次数+1，否则设为1 map.put(sum, map.getOrDefault(sum, 0) + 1); } return ans; } ","date":"2026-01-30T22:30:00Z","permalink":"https://iamxurulin.github.io/p/%E5%89%8D%E7%BC%80%E5%92%8C-%E5%93%88%E5%B8%8Clcr-010.-%E5%92%8C%E4%B8%BA-k-%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84/","title":"【前缀和+哈希】LCR 010. 和为 K 的子数组"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public int numSubarrayProductLessThanK(int[] nums, int k) { int cur = 1; int left = 0; int right = 0; int res = 0; // 每次循环都在考虑：以 right 为右边界的所有满足条件的子数组 while (right \u0026lt; nums.length) { // 把 nums[right] 包含进来 cur *= nums[right]; // 调整左边界，保证窗口乘积 \u0026lt; k while (left \u0026lt;= right \u0026amp;\u0026amp; cur \u0026gt;= k) { // 从窗口中移除 nums[left] cur /= nums[left]; // 左边界右移，缩小窗口 left++; } // 计算以 right 为右边界的满足条件的子数组个数 res += (right - left + 1); // 右指针右移，准备处理下一个右边界 right++; } return res; } 小贴士 关键在于以 right 为结尾的、乘积 \u0026lt; k 的子数组有right - left + 1 个\n","date":"2026-01-30T21:50:40Z","permalink":"https://iamxurulin.github.io/p/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3lcr-009.-%E4%B9%98%E7%A7%AF%E5%B0%8F%E4%BA%8E-k-%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84/","title":"【滑动窗口】LCR 009. 乘积小于 K 的子数组"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public static int minSubArrayLen(int target, int[] nums) { // 获取原数组长度 int n = nums.length; int[] preSum = new int[n + 1]; preSum[0] = 0; for (int i = 1; i \u0026lt;= n; i++) { preSum[i] = preSum[i - 1] + nums[i - 1]; } int left = 0; int right = 1; int ans = Integer.MAX_VALUE; while (right \u0026lt; preSum.length) { // 当前窗口的和 \u0026gt;= target if (preSum[right] - preSum[left] \u0026gt;= target) { // 更新最小长度 ans = Math.min(right - left, ans); // 既然当前窗口已经满足条件，尝试去掉左边元素，看能否找到更短的窗口 left++; } else if (preSum[right] - preSum[left] \u0026lt; target) { // 当前窗口不满足条件，和太小了 // 右指针右移，将更多元素包含进窗口 right++; } } return ans == Integer.MAX_VALUE ? 0 : ans; } ","date":"2026-01-30T21:16:06Z","permalink":"https://iamxurulin.github.io/p/%E5%89%8D%E7%BC%80%E5%92%8C-%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3lcr-008.-%E9%95%BF%E5%BA%A6%E6%9C%80%E5%B0%8F%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84/","title":"【前缀和+滑动窗口】LCR 008. 长度最小的子数组"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public int[] twoSum(int[] numbers, int target) { for(int i=0;i\u0026lt;numbers.length;i++){ int left = i+1; int right = numbers.length-1; //二分查找是闭区间 [left, right]，等号保证最后一个元素被检查，避免漏查 while (left\u0026lt;=right) { int mid = left+((right-left)\u0026gt;\u0026gt;1); if(target-numbers[i]==numbers[mid]){ return new int[]{i,mid}; }else if(target-numbers[i]\u0026gt;numbers[mid]){ left=mid+1; }else{ right=mid-1; } } } return new int[]{-1,-1}; } 小贴士 前文【哈希】两数之和 是使用哈希做的，这次利用有序的特性，通过二分的方法完成。\n","date":"2026-01-30T20:05:59Z","permalink":"https://iamxurulin.github.io/p/lcr-006-%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8Cii-%E8%BE%93%E5%85%A5%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84/","title":"LCR-006-两数之和II-输入有序数组"},{"content":"1. Node.js 让JavaScript脱离浏览器，能运行在服务器/本地终端\n举个🌰：\n你写了一个app.js文件（里面是console.log('hello')），没有Node.js的话，这个文件只能在浏览器里运行；有了Node.js，就能在终端执行它。\nnpm、node都依赖Node.js存在。\n2. node 安装Node.js后，系统自动添加的终端命令，用来启动Node.js运行环境\nnode是操作Node.js的“入口”，就像你打开微信的“图标”，图标本身不是微信，只是启动微信的方式。\n工作或学习中，很多人说“装node”，其实说的是“装Node.js”。\n3. npm Node Package Manager的缩写，Node.js安装后默认自带的包管理工具。\nnpm是Node.js的一部分，必须先装Node.js，才会有npm。\n4. nvm Node Version Manager的缩写，第三方开发的Node.js版本管理工具\n由于不同项目可能会依赖不同版本的Node.js，比如：\nA项目要Node16，B项目要Node20，nvm可以安装多个Node.js版本，并且可以通过一条命令（nvm use 20）瞬间切换当前使用的版本。\nnvm是管理Node.js的“工具”，本身并不包含Node.js，需要通过nvm安装具体的Node.js版本。\n","date":"2026-01-30T17:24:30Z","permalink":"https://iamxurulin.github.io/p/node.jsnpmnodenvm%E7%9A%84%E5%8C%BA%E5%88%AB/","title":"Node.js、npm、node、nvm的区别"},{"content":"由于 Swagger 对流式输出（Server-Sent Events）的测试不够直观，本文介绍如何使用 curl 命令行工具测试 SSE 接口。\n前提条件： Windows 11 专业版\nGit for Windows（自带 Git Bash 和 curl）\n后端服务运行在 http://localhost:8080\n有效的用户账号和密码\n测试步骤 1.任意进入一个文件夹，【右键】选择【Open Git Bash here】 2.执行以下命令，进行登录，获取 Cookie，并将 Cookie 保存到 cookies.txt 文件中\n1 2 3 4 5 6 7 curl -X POST \u0026#34;http://localhost:8080/api/user/login\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{ \u0026#34;userAccount\u0026#34;: \u0026#34;CodeEdge_Xu\u0026#34;, \u0026#34;userPassword\u0026#34;: \u0026#34;xxxxxxxx\u0026#34; }\u0026#39; \\ -c cookies.txt 3.调用生成代码接口（流式）\n⚠️注意：\n需要先从Swagger中获取appId和message，测试已经创建好的应用ID。\n1 2 3 4 5 6 7 curl -G \u0026#34;http://localhost:8080/api/app/chat/gen/code\u0026#34; \\ --data-urlencode \u0026#34;appId=xxxx\u0026#34; \\ --data-urlencode \u0026#34;message=做一个大模型笔记网站，总代码行数不超过30\u0026#34; \\ -H \u0026#34;Accept: text/event-stream\u0026#34; \\ -H \u0026#34;Cache-Control: no-cache\u0026#34; \\ -b cookies.txt \\ --no-buffer ","date":"2026-01-30T16:25:03Z","permalink":"https://iamxurulin.github.io/p/%E4%BD%BF%E7%94%A8-curl-%E6%B5%8B%E8%AF%95%E6%B5%81%E5%BC%8F%E8%BE%93%E5%87%BA%E6%8E%A5%E5%8F%A3sse/","title":"使用 curl 测试流式输出接口（SSE）"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public int maxProduct(String[] words) { int n = words.length; int[] masks = new int[n]; int[] lens = new int[n]; for (int i = 0; i \u0026lt; n; i++) { lens[i] = words[i].length(); for (char c : words[i].toCharArray()) { masks[i] |= 1 \u0026lt;\u0026lt; (c - \u0026#39;a\u0026#39;); } } int max = 0; for (int i = 0; i \u0026lt; n; i++) { for (int j = i + 1; j \u0026lt; n; j++) { if ((masks[i] \u0026amp; masks[j]) == 0) { max = Math.max(max, lens[i] * lens[j]); } } } return max; } 小贴士 遍历每个字符串，用一个 int 整数（掩码）表示该字符串包含的字符。\n解释一下：\n如果二进制第c-\u0026lsquo;a\u0026rsquo;位为 1，则表示包含字符c。\n比如包含a则第 0 位为 1，包含b则第 1 位为 1。\n","date":"2026-01-29T23:59:13Z","permalink":"https://iamxurulin.github.io/p/lcr005-%E6%9C%80%E5%A4%A7%E5%8D%95%E8%AF%8D%E9%95%BF%E5%BA%A6%E4%B9%98%E7%A7%AF/","title":"LCR005-最大单词长度乘积"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public int singleNumber(int[] nums) { int ans = 0; // 遍历int的每一个二进制位，i表示当前处理第i位（0是最低位，31是最高位） for (int i = 0; i \u0026lt; 32; ++i) { int total = 0; // 统计当前第i位的总1数 for (int num : nums) { // 提取num的第i位值（0或1）并累加 // (num \u0026gt;\u0026gt; i)：将num的第i位移到最低位 // \u0026amp; 1：保留最低位，消去其他位，得到第i位的实际值（0/1） total += ((num \u0026gt;\u0026gt; i) \u0026amp; 1); } // 总1数%3≠0 → 唯一数的第i位是1 if (total % 3 != 0) { // 将1写入ans的第i位：1\u0026lt;\u0026lt;i得到第i位为1、其余位为0的数，再和ans做或运算 ans |= (1 \u0026lt;\u0026lt; i); } } return ans; } 小贴士 对于二进制的每一位（0~31 位），数组中所有数的该位上的 1，只会来自两部分： 出现 3 次的数的该位 1 ➕ 出现 1 次的数的该位 1。\n由于 3 次的数的 1 相加后，总数一定是 3 的倍数，因此该位总 1 数 %3 的结果，就是唯一数在该位的取值（0 或 1）。\n","date":"2026-01-29T23:22:30Z","permalink":"https://iamxurulin.github.io/p/lcr004-%E5%8F%AA%E5%87%BA%E7%8E%B0%E4%B8%80%E6%AC%A1%E7%9A%84%E6%95%B0%E5%AD%97ii/","title":"LCR004-只出现一次的数字II"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public int[] countBits(int n) { int[] bits = new int[n+1]; for(int i=0;i\u0026lt;=n;i++){ bits[i]=oneBits(i); } return bits; } private int oneBits(int n){ int count = 0; while(n\u0026gt;0){ n\u0026amp;=(n-1); count++; } return count; } 小贴士 核心就是用 n \u0026amp; (n-1) 消去最右侧 1的技巧统计 1 的个数。\n","date":"2026-01-29T22:31:28Z","permalink":"https://iamxurulin.github.io/p/lcr003-%E6%AF%94%E7%89%B9%E4%BD%8D%E8%AE%A1%E6%95%B0/","title":"LCR003-比特位计数"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public String addBinary(String a, String b) { // 如果第一个字符串为空/长度为0，直接返回第二个字符串 if (a.length() \u0026lt;= 0) { return b; } // 如果第二个字符串为空/长度为0，直接返回第一个字符串 if (b.length() \u0026lt;= 0) { return a; } int i = a.length() - 1; int j = b.length() - 1; // 定义进位变量tmp int tmp = 0; // 定义StringBuilder拼接结果 StringBuilder sb = new StringBuilder(); // 需要注意：最后一位相加仍有进位时，需把进位1也拼接到结果 while (i \u0026gt;= 0 || j \u0026gt;= 0 || tmp != 0) { tmp += i \u0026gt;= 0 ? a.charAt(i--) - \u0026#39;0\u0026#39; : 0; tmp += j \u0026gt;= 0 ? b.charAt(j--) - \u0026#39;0\u0026#39; : 0; // 取余2：得到当前位的计算结果 sb.append(tmp % 2); // 除以2：更新进位值 tmp = tmp / 2; } // 结果逆序：因为是从个位开始拼接，需要反转回正序，再转字符串返回 return sb.reverse().toString(); } ###小贴士\n这道题本质上处理方式和【字节面试手撕】大数加法是一样的。\n","date":"2026-01-29T21:44:50Z","permalink":"https://iamxurulin.github.io/p/lcr002-%E4%BA%8C%E8%BF%9B%E5%88%B6%E6%B1%82%E5%92%8C/","title":"LCR002-二进制求和"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 //定义移位边界，防止左移溢出 private static final int BOUND = Integer.MIN_VALUE\u0026gt;\u0026gt;1; //被除数是最小负数，除数是-1，返回最大正数 public int divide(int a,int b){ if(a==Integer.MIN_VALUE\u0026amp;\u0026amp;b==-1){ return Integer.MAX_VALUE; } if(a==0||b==1){ return a; }else if(b == -1){ return -a; } int negative = 2; // 统一转为负数计算，避免MIN_VALUE取反溢出 if(a\u0026gt;0){ negative--; a=-a; } //仅处理a、b均为负数的情况，返回正的商 if(b\u0026gt;0){ negative--; b=-b; } int ans = helpDivide(a,b); return negative == 1 ? -ans : ans; } private int helpDivide(int a,int b){ // 被除数绝对值 == 除数绝对值，商为1 if(a==b){ return 1; } int res = 0; int shift = getMaxShift(a,b); while(a\u0026lt;=b){ while (a\u0026gt;(b\u0026lt;\u0026lt;shift)) { shift--; } a-=(b\u0026lt;\u0026lt;shift);// 减去 b×2^shift res+=(1\u0026lt;\u0026lt;shift);// 商累加 2^shift } return res; } // 获取除数b的最大有效移位次数 private int getMaxShift(int a,int b){ int shift = 0; int tmp = b; while(tmp\u0026gt;a \u0026amp;\u0026amp; tmp\u0026gt;=BOUND){ tmp\u0026lt;\u0026lt;=1; shift++; } return shift; } 小贴士 必须以除数为起点左移（倍增），而非被除数。\nInteger.MIN_VALUE = -2^31 取反会溢出。\n两个负数相比较时，a更小表示绝对值更大。\n","date":"2026-01-29T21:30:34Z","permalink":"https://iamxurulin.github.io/p/lcr001-%E4%B8%A4%E6%95%B0%E7%9B%B8%E9%99%A4/","title":"LCR001-两数相除"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public int[][] rotateMatrix(int[][] mat, int n) { // 空矩阵、空方阵直接返回 if (mat == null || n == 0) { return mat; } // 矩阵转置（行和列互换） for (int i = 0; i \u0026lt; n; i++) { for (int j = i; j \u0026lt; n; j++) { int tmp = mat[i][j]; mat[i][j] = mat[j][i]; mat[j][i] = tmp; } } // 反转转置后矩阵的每一行，完成顺时针90度旋转 for (int[] row : mat) { reverse(row); } return mat; } // 双指针法原地反转一维数组 private void reverse(int[] arr) { int left = 0; int right = arr.length - 1; while (left \u0026lt; right) { // 交换左右指针元素 int tmp = arr[left]; arr[left] = arr[right]; arr[right] = tmp; left++; right--; } } 小贴士 j 从 0 开始会让对角线上下的元素对被交换两次，交换两次就等于没交换，而j从i开始能保证每个元素对只被交换一次。\n","date":"2026-01-28T21:03:56Z","permalink":"https://iamxurulin.github.io/p/%E6%A8%A1%E6%8B%9F%E9%A1%BA%E6%97%B6%E9%92%88%E6%97%8B%E8%BD%AC%E7%9F%A9%E9%98%B5/","title":"【模拟】顺时针旋转矩阵"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public ArrayList\u0026lt;Integer\u0026gt; spiralOrder(int[][] matrix) { ArrayList\u0026lt;Integer\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); // 处理空矩阵、空行、空列场景，避免空指针/数组越界 if (matrix == null || matrix.length == 0 || matrix[0].length == 0) { return ans; } int m = matrix.length; // 矩阵行数 int n = matrix[0].length; // 矩阵列数 // 定义矩阵四个边界指针，初始指向边缘 int left_bound = 0; int right_bound = n - 1; int up_bound = 0; int down_bound = m - 1; // 遍历所有元素后终止循环 while (ans.size() \u0026lt; m * n) { // 方向1：从左到右遍历上边界行，遍历后上边界向下收缩 if (up_bound \u0026lt;= down_bound) { for (int j = left_bound; j \u0026lt;= right_bound; j++) { ans.add(matrix[up_bound][j]); } up_bound++; } // 方向2：从上到下遍历右边界列，遍历后右边界向左收缩 if (left_bound \u0026lt;= right_bound) { for (int i = up_bound; i \u0026lt;= down_bound; i++) { ans.add(matrix[i][right_bound]); } right_bound--; } // 方向3：从右到左遍历下边界行，遍历后下边界向上收缩 if (up_bound \u0026lt;= down_bound) { for (int j = right_bound; j \u0026gt;= left_bound; j--) { ans.add(matrix[down_bound][j]); } down_bound--; } // 方向4：从下到上遍历左边界列，遍历后左边界向右收缩 if (left_bound \u0026lt;= right_bound) { for (int i = down_bound; i \u0026gt;= up_bound; i--) { ans.add(matrix[i][left_bound]); } left_bound++; } } return ans; } ","date":"2026-01-28T20:33:10Z","permalink":"https://iamxurulin.github.io/p/%E6%A8%A1%E6%8B%9F%E8%9E%BA%E6%97%8B%E7%9F%A9%E9%98%B5/","title":"【模拟】螺旋矩阵"},{"content":"在GITHUB仓库上传图片并获取URL 这篇博客中介绍了如何搭建自己的图床，现在收集了一些图片，想更新一下里面的图片。\n那如何把本地文件夹和已存在的 GitHub 远程仓库同步呢？\n现在的问题是：\nGitHub 上已经有一个仓库，里面有一些图片+README.md\n本地已经建好了一个文件夹，里面放了很多其他图片\n我希望先把远程仓库的文件拉到本地，再把本地新增的图片推送上去，最终实现双向同步。\n下面分享一个安全且不容易出错的流程，亲测可用。\n1.进入现在的本地文件夹 1 cd /path/to/本地图片文件夹 2.初始化并关联远程 1 2 git init git remote add origin git@github.com:/你的用户名/你的仓库名.git 推荐使用ssh，原因和操作可参考配置ssh解决https不稳定的问题 。\n3.拉取远程内容 1 2 3 4 5 # 从名叫 “origin” 的远程仓库（通常就是你的GitHub仓库）拉取（fetch）最新的数据 git fetch origin #把你的工作目录切换（checkout）到名叫 “main” 的分支 git checkout main 这一步会把远程的文件下载到本地。\n这里解释一下git fetch跟 git pull 的区别：\ngit fetch：只下载，不合并（安全、无副作用）\ngit pull：相当于 git fetch + git merge（下载完立刻合并到当前分支，可能会改你本地文件）\n4.添加本地文件并推送 1 2 3 git add . git commit -m \u0026#34;合并本地图片到远程仓库\u0026#34; git push origin main 以上方法亲测可行，当然，如果害怕出错，最稳的做法是先 clone 远程仓库到一个新目录➡️复制本地文件进去 ➡️commit \u0026amp; push。\n这样既能保留远程的文件，又能安全地把本地新增内容上传上去，避免各种奇怪的冲突。\n","date":"2026-01-28T17:13:44Z","permalink":"https://iamxurulin.github.io/p/%E5%A6%82%E4%BD%95%E6%8A%8A%E6%9C%AC%E5%9C%B0%E6%96%87%E4%BB%B6%E5%A4%B9%E5%92%8C%E5%B7%B2%E5%AD%98%E5%9C%A8%E7%9A%84-github-%E8%BF%9C%E7%A8%8B%E4%BB%93%E5%BA%93%E5%90%8C%E6%AD%A5/","title":"如何把本地文件夹和已存在的 GitHub 远程仓库同步"},{"content":"在前后端分离的开发中，数据库里存的主键 ID 明明是：1750433246798835714，但是前端接收到数据后，ID 却变成了：1750433246798835700，后几位莫名其妙变成了 0！\n导致的结果就是：\n前端拿这个错误的 ID 去查询详情或执行删除操作时，后端直接报错“数据不存在”，因为 ID 对不上。\n原因分析 这个问题的根源在于 Java 和 JavaScript 对数字处理机制的不同。\nJava 中的 Long 类型是 64 位有符号整数，它的取值范围非常大；\n而常用的雪花算法 (Snowflake) 生成的 ID 通常就是 19 位的，完全在 Java 的 Long 范围内。\nJavaScript 中并没有专门的整数类型，所有的数字本质上都是 IEEE 754 标准的双精度浮点数（Double）。\nJS 能安全表示的最大整数（Safe Integer）是253-1,这就导致后端的 19 位 ID传给前端时，因为超过了 JS 的安全整数范围，JS 就会发生精度丢失，自动进行“四舍五入”或者直接丢弃低位，导致后几位变成 0。\n解决方案 要解决这个问题，最简单有效的方法是：\n后端在返回数据给前端时，把 Long 类型的数据统统转换成 String类型。\n因为字符串在 JS 里是绝对安全的，不会发生精度丢失。\n不需要手动去改每一个 DTO 或 VO 类，只需要在 Spring Boot 中加一个全局配置类，利用 Jackson 的序列化机制自动完成转换。\n直接在 config 包下新建一个类JsonConfig.java：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 /** * Spring MVC全局Json配置 * 用于解决 Long 类型精度丢失问题 */ @JsonComponent public class JsonConfig { /** * 全局配置：将 Long 类型序列化为 String */ @Bean public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { // 创建 ObjectMapper ObjectMapper objectMapper = builder.createXmlMapper(false).build(); // 创建自定义序列化模块 SimpleModule module = new SimpleModule(); // 将 Long 类（包装类）和 long（基本类型）都序列化为字符串 // ToStringSerializer.instance 会调用对象的 toString() 方法 module.addSerializer(Long.class, ToStringSerializer.instance); module.addSerializer(Long.TYPE, ToStringSerializer.instance); // 注册模块 objectMapper.registerModule(module); return objectMapper; } } 使用这种全局配置 方案，可以一劳永逸地解决项目中所有 Long 类型精度丢失的问题。\n","date":"2026-01-28T13:23:17Z","permalink":"https://iamxurulin.github.io/p/%E5%90%8E%E7%AB%AF-long-%E7%B1%BB%E5%9E%8B-id-%E4%BC%A0%E7%BB%99%E5%89%8D%E7%AB%AF%E7%B2%BE%E5%BA%A6%E4%B8%A2%E5%A4%B1%E5%8F%9800%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/","title":"后端 Long 类型 ID 传给前端精度丢失（变00）的解决方案"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public int[] solve(int n, int m, int[] a) { m %= n; reverse(a, 0, n - 1); reverse(a, 0, m - 1); reverse(a, m, n - 1); return a; } private void reverse(int[] arr, int start, int end) { while (start \u0026lt; end) { swap(arr, start++, end--); } } private void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } 小贴士 因为右移 n 位 = 没动，右移 n + k 位 = 右移 k 位；\n所以，要通过取余操作把 m 压缩到 [0, n-1] 范围。\n","date":"2026-01-27T23:25:59Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%89%E6%AC%A1%E7%BF%BB%E8%BD%AC%E6%97%8B%E8%BD%AC%E6%95%B0%E7%BB%84/","title":"【三次翻转】旋转数组"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public int minmumNumberOfHost (int n, int[][] startEnd) { Arrays.sort(startEnd,(a,b)-\u0026gt;{ if(a[0]==b[0]){ return Integer.compare(a[1], b[1]); } return Integer.compare(a[0], b[0]); }); PriorityQueue\u0026lt;Integer\u0026gt; queue = new PriorityQueue\u0026lt;\u0026gt;(); int maxHost = 0; for(int[] activity:startEnd){ while(!queue.isEmpty()\u0026amp;\u0026amp;queue.peek()\u0026lt;=activity[0]){ queue.poll(); } queue.offer(activity[1]); maxHost = Math.max(maxHost,queue.size()); } return maxHost; } 踩坑记录 ❌1.排序比较器整数溢出\n1 2 3 Arrays.sort(startEnd, (a, b) -\u0026gt; (a[0] == b[0] ? b[1] - a[1] : a[0] - b[0]) ); ❌2.返回值不能返回最后的堆大小\n1 return queue.size(); // 因为题目要的是整个过程中，堆出现过的最大大小。\n❌3.释放主持人只用 if可能不够\n1 2 3 if (queue.peek() \u0026lt;= activity[0]) { queue.poll(); } 因为实际情况下可能有多个活动已经结束。\n","date":"2026-01-27T23:08:23Z","permalink":"https://iamxurulin.github.io/p/%E4%BC%98%E5%85%88%E7%BA%A7%E9%98%9F%E5%88%97%E4%B8%BB%E6%8C%81%E4%BA%BA%E8%B0%83%E5%BA%A6%E4%BA%8C/","title":"【优先级队列】主持人调度（二）"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public int candy(int[] arr) { if (arr == null || arr.length == 0) { return 0; } int n = arr.length; int[] candyCount = new int[n]; // 定义数组记录每个孩子最终应分得的糖果数 int ans = 0; // 统计分发糖果的总数量 // 每个孩子至少分得1颗糖果 for(int i=0;i\u0026lt;arr.length;i++){ candyCount[i]=1; } // 从左到右遍历数组，保证相邻孩子中，右侧评分更高的孩子糖果数多于左侧 for(int i=1;i\u0026lt;n;i++){ if(arr[i] \u0026gt; arr[i-1]){ candyCount[i] = candyCount[i-1] + 1; } } // 从右到左遍历数组，保证相邻孩子中，左侧评分更高的孩子糖果数多于右侧 for(int i=n-2;i\u0026gt;=0;i--){ if(arr[i] \u0026gt; arr[i+1] \u0026amp;\u0026amp; candyCount[i] \u0026lt;= candyCount[i+1]){ candyCount[i] = candyCount[i+1] + 1; } } // 累加所有孩子的糖果数，得到分发的总数量 for (int i = 0; i \u0026lt; n; i++) { ans += candyCount[i]; } // 返回总糖果数 return ans; } ","date":"2026-01-27T22:09:03Z","permalink":"https://iamxurulin.github.io/p/%E6%95%B0%E7%BB%84%E5%88%86%E7%B3%96%E6%9E%9C%E9%97%AE%E9%A2%98/","title":"【数组】分糖果问题"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public long maxWater(int[] arr) { // 数组长度\u0026lt;3，无法形成凹坑接水，直接返回0 if (arr == null || arr.length \u0026lt; 3) { return 0; } int left = 0; // 左指针 int right = arr.length - 1; // 右指针 long ans = 0; // 总积水量 int leftMax = arr[left]; // 左指针左侧（含当前）的最大柱子高度，初始为左指针初始值 int rightMax = arr[right]; // 右指针右侧（含当前）的最大柱子高度，初始为右指针初始值 while (left \u0026lt; right) { // 更新左右最大高度→取最大值，记录遍历过的最高柱子 leftMax = Math.max(leftMax, arr[left]); rightMax = Math.max(rightMax, arr[right]); // 当前位置的积水量由较小的最大高度决定，移动该侧指针可逐步统计积水量 if (leftMax \u0026lt; rightMax) { ans += leftMax - arr[left]; left++; } else { ans += rightMax - arr[right]; right--; } } return ans; // 返回总积水量 } ","date":"2026-01-27T21:13:29Z","permalink":"https://iamxurulin.github.io/p/%E5%8F%8C%E6%8C%87%E9%92%88%E6%8E%A5%E9%9B%A8%E6%B0%B4/","title":"【双指针】接雨水"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public int maxArea(int[] height) { int left = 0; // 左指针 int right = height.length - 1; // 右指针 int ans = 0; // 记录最大面积，初始为0（面积非负） // 双指针相向遍历，直到指针相遇 while (left \u0026lt; right) { // 计算当前容器面积并更新最大值：宽度(左右指针间距) × 有效高度(矮柱子决定，取最小值) ans = Math.max(ans, (right - left) * Math.min(height[right], height[left])); // 移动更矮的柱子指针（只有这样才有可能增大有效高度，获得更大面积） if (height[right] \u0026lt; height[left]) { right--; } else { left++; } } return ans; // 返回最大面积 } ","date":"2026-01-27T20:18:39Z","permalink":"https://iamxurulin.github.io/p/%E5%8F%8C%E6%8C%87%E9%92%88%E7%9B%9B%E6%B0%B4%E6%9C%80%E5%A4%9A%E7%9A%84%E5%AE%B9%E5%99%A8/","title":"【双指针】盛水最多的容器"},{"content":"运行Spring Boot工程代码出现以下报错：\n1 位置: 类型为com.xx.xx.exception.ErrorCode的变量 errorCode 解决方法 看截图中间那个路径框：\n1 ...lombok\\unknown\\lombok-unknown.jar 这里的 unknown 说明 IDEA 根本没找到 Lombok 的 jar 包。\n接下来，\n把选中的 “处理器路径” 改为上面那个选项 —— “从项目类路径获取处理器 ”。\n因为项目是 Maven 管理的，Maven 已经下载好了 Lombok（在 pom.xml 里），这样的话 IDEA就可以直接用 Maven 下载好的那个包了。\n保存并重试，问题解决。\n","date":"2026-01-27T12:56:48Z","permalink":"https://iamxurulin.github.io/p/java-%E6%89%BE%E4%B8%8D%E5%88%B0%E7%AC%A6%E5%8F%B7%E6%96%B9%E6%B3%95-getcode/","title":"java: 找不到符号方法 getCode()"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public int maxLength(int[] arr) { // 记录「当前滑动窗口内」每个元素的出现次数 HashMap\u0026lt;Integer, Integer\u0026gt; window = new HashMap\u0026lt;\u0026gt;(); int left = 0, right = 0; // 滑动窗口双指针，「左闭右开」区间 int valid = 0; // 记录最长无重复子数组的长度 while (right \u0026lt; arr.length) { int c = arr[right]; // 即将加入窗口的当前元素 right++; // 右指针右移，扩大窗口 // 更新窗口内元素计数 window.put(c, window.getOrDefault(c, 0) + 1); // 当【当前元素】在窗口内重复（次数\u0026gt;1），收缩左指针，直到窗口内无重复 while (window.get(c) \u0026gt; 1) { int d = arr[left]; // 即将移出窗口的左指针元素 left++; // 左指针右移，收缩窗口 window.put(d, window.get(d) - 1); // 移出元素的计数-1 } // 更新最长无重复窗口的长度 valid = Math.max(valid, right - left); } // 返回最终的最长长度 return valid; } ","date":"2026-01-26T23:57:22Z","permalink":"https://iamxurulin.github.io/p/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E9%95%BF%E6%97%A0%E9%87%8D%E5%A4%8D%E5%AD%90%E6%95%B0%E7%BB%84/","title":"【滑动窗口】最长无重复子数组"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public String solve(String str) { // 空串/NULL直接返回 if (str == null || str.length() == 0) { return str; } int i = 0; // 左指针 int j = str.length() - 1; // 右指针 char[] s = str.toCharArray(); // 由于String不可变,需要将字符串转成字符数组 // 首尾双指针相向遍历 while (i \u0026lt; j) { char c = s[i]; s[i] = s[j]; s[j] = c; i++; j--; } // 字符数组转回字符串返回 return new String(s); } ","date":"2026-01-26T23:11:14Z","permalink":"https://iamxurulin.github.io/p/%E5%8F%8C%E6%8C%87%E9%92%88%E5%8F%8D%E8%BD%AC%E5%AD%97%E7%AC%A6%E4%B8%B2/","title":"【双指针】反转字符串"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public String minWindow(String S, String T) { // 记录当前窗口中各字符的出现次数 HashMap\u0026lt;Character, Integer\u0026gt; window = new HashMap\u0026lt;\u0026gt;(); // 记录T中各字符需要的出现次数 HashMap\u0026lt;Character, Integer\u0026gt; need = new HashMap\u0026lt;\u0026gt;(); // 统计T中每个字符的出现次数 for (char c : T.toCharArray()) { need.put(c, need.getOrDefault(c, 0) + 1); } int left = 0, right = 0; // 滑动窗口的左右指针，[left, right) 左闭右开区间 int valid = 0; // 有效匹配数 int start = 0, len = Integer.MAX_VALUE; // 记录最小窗口的起始下标和长度，初始len为无穷大 // 右指针扩窗口 → 嵌套 左指针缩窗口 while (right \u0026lt; S.length()) { char c = S.charAt(right); // 即将加入窗口的字符 right++; // 右指针右移，扩大窗口（左闭右开，先取字符再移指针） // 若当前字符是T需要的字符，更新window表 if (need.containsKey(c)) { window.put(c, window.getOrDefault(c, 0) + 1); // 当window中该字符的次数 == need中需要的次数时，有效匹配数+1 if (window.get(c).equals(need.get(c))) { valid++; } } // 当窗口满足条件（包含T所有字符），开始收缩左指针，找最小窗口 while (valid == need.size()) { if (right - left \u0026lt; len) { // 若当前窗口长度更小，更新start和len start = left; // 记录最小窗口的起始下标 len = right - left; // 记录最小窗口的长度（左闭右开，长度=right-left） } char d = S.charAt(left); // 即将移出窗口的字符 left++; // 若移出的字符是T需要的字符，更新window表和valid if (need.containsKey(d)) { // 若移出前该字符的次数刚好满足need，移出后会不满足，有效匹配数-1 if (window.get(d).equals(need.get(d))) { valid--; } window.put(d, window.get(d) - 1); // 窗口中该字符次数-1 } } } // 若len仍为无穷大，说明无满足条件的窗口，返回空串；否则返回最小子串 return len == Integer.MAX_VALUE ? \u0026#34;\u0026#34; : S.substring(start, start + len); } 小贴士 因为滑动窗口是左闭右开区间，所有len的计算不用+1。\n","date":"2026-01-26T22:58:01Z","permalink":"https://iamxurulin.github.io/p/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%B0%8F%E8%A6%86%E7%9B%96%E5%AD%90%E4%B8%B2/","title":"【滑动窗口】最小覆盖子串"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public ArrayList\u0026lt;Interval\u0026gt; merge(ArrayList\u0026lt;Interval\u0026gt; intervals) { ArrayList\u0026lt;Interval\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); // 边界处理：入参为null或空集合，直接返回空 if (intervals == null || intervals.size() == 0) { return ans; } // 按区间的start值升序排序 Collections.sort(intervals, (a, b) -\u0026gt; (a.start - b.start)); // 初始化【当前合并区间】为排序后的第一个区间 Interval current = intervals.get(0); for (int i = 1; i \u0026lt; intervals.size(); i++) { // 取出当前遍历到的待对比区间 Interval temp = intervals.get(i); // 重叠判断：待对比区间的start ≤ 当前合并区间的end → 两个区间重叠，需要合并 if (temp.start \u0026lt;= current.end) { // 更新当前合并区间的end为两者end的最大值 current.end = Math.max(current.end, temp.end); } else { // 将当前合并完成的区间加入结果集合 ans.add(current); // 更新「当前合并区间」为当前遍历到的新区间，继续后续合并 current = temp; } } // 手动将最后一个未加入的「当前合并区间」加入结果 ans.add(current); // 返回最终合并结果 return ans; } 小贴士 遍历中只有遇到非重叠区间时，才会把 上一个 current加入结果，而最后一个current没有后续区间对比，遍历结束后不会被自动加入，所以需要手动补充。\n","date":"2026-01-26T21:05:27Z","permalink":"https://iamxurulin.github.io/p/%E5%BF%AB%E6%89%8B%E6%89%8B%E6%92%95%E5%90%88%E5%B9%B6%E5%8C%BA%E9%97%B4/","title":"【快手手撕】合并区间"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 public boolean judge(String str) { int n = str.length(); // 获取字符串长度，用于定义右指针初始位置 // 双指针遍历 for (int i = 0, j = n - 1; i \u0026lt; j; i++, j--) { // 逐位对比首尾字符，只要有一位不一致，直接判定不是回文 if (str.charAt(i) != str.charAt(j)) { return false; } } // 所有对应位字符都一致，判定是回文 return true; } ","date":"2026-01-26T19:48:17Z","permalink":"https://iamxurulin.github.io/p/%E5%8F%8C%E6%8C%87%E9%92%88%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E4%B8%BA%E5%9B%9E%E6%96%87%E5%AD%97%E7%AC%A6%E4%B8%B2/","title":"【双指针】判断是否为回文字符串"},{"content":"在IDEA开发环境中，点击提交出现以下警告： CR：Carriage Return（回车） LF：Line Feed（换行） 因为在Windows 系统中默认使用 CRLF（回车+换行）作为一行的结束，但是，在Linux/Mac 系统/Git 内部，默认使用 LF（仅换行）作为一行的结束。\n这个警告的意思是当前的文件里包含 Windows 风格的换行符 (CRLF)。\n而Git 的标准规范建议：\n在仓库（服务器）里统一保存为 LF 格式，以保证不管是谁（用 Windows 还是 Mac 的同事）拉取代码都能正常工作。\n如果点击“修复并提交”，则会执行命令：\n1 git config --global core.autocrlf true 这样的话，提交时会自动把本地的 CRLF 转换成 LF 存入仓库，保持仓库的纯净。\n在拉取时，会自动把仓库里的 LF 转换回 CRLF 给 Windows 系统用，保证在本地看代码不会出现乱码。\n","date":"2026-01-26T16:20:07Z","permalink":"https://iamxurulin.github.io/p/crlf%E4%B8%8Elf%E7%9A%84%E8%A1%8C%E5%88%86%E9%9A%94%E7%AC%A6%E8%AD%A6%E5%91%8A%EF%B8%8F/","title":"CRLF与LF的行分隔符警告⚠️"},{"content":"在开发Spring Boot项目中，如果我们想把自己的项目开源到Github仓库，application.yml中的某些配置比如MySQL、Redis的账户密码，还有的就是现在引入AI之后的一些密钥，可能都不太愿意推送到仓库去。\n这个时候，可以通过配置多环境来解决，给本地一个专有的配置文件，然后将其添加到.gitignore文件中，这样就有效解决了这个问题。\n下面介绍一下 Spring Boot的多环境配置方法：\nSpring Boot 支持的多环境配置文件，命名规则为 application-{profile}.yml（profile 是环境标识，如local/dev/test/prod）。\n操作步骤 application.yml（公共配置） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 spring: application: name: xxx profiles: active: local # 激活local环境 # springdoc-openapi（所有环境都用这个扫描包，通用） springdoc: group-configs: - group: \u0026#39;default\u0026#39; packages-to-scan: com.xx.xx.controller # knife4j（所有环境都开启，中文界面，通用） knife4j: enable: true setting: language: zh_cn application-local.yml（本地开发专属配置） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 本地专属：端口、上下文路径 server: port: xxxx servlet: context-path: /api # 本地专属的其他配置（比如数据库、日志、Redis等） # spring: # datasource: # url: jdbc:mysql://localhost:3306/xx?useSSL=false\u0026amp;serverTimezone=UTC # username: xx # password: xx # driver-class-name: com.mysql.cj.jdbc.Driver # logging: # level: # com.xx.xx: debug # 本地开发开启debug日志 # redis: # host: localhost # port: xxxx 通过在 application.yml 中配置 spring.profiles.active 激活指定环境，最后的加载顺序就是：\n先加载公共配置 application.yml；\n再加载激活的环境配置 application-local.yml；\n这样的话，环境配置中的同名配置项会覆盖公共配置，不同配置项自动合并，实现 “公共 + 专属” 并存。\n","date":"2026-01-26T15:27:48Z","permalink":"https://iamxurulin.github.io/p/spring-boot%E7%9A%84%E5%A4%9A%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE/","title":"Spring Boot的多环境配置"},{"content":"在IDEA中新建Spring Boot项目时，需要选择JDK版本，这里小记一下：\nOracle OpenJDK Oracle 官方维护的开源免费JDK，Java标准实现。\nAmazon Corretto 亚马逊基于 OpenJDK 定制的免费、长期支持JDK。\nJetBrains Runtime JetBrains（IDEA/PyCharm 等 IDE 厂商）专为 IDE 自身运行定制的 JDK。\n","date":"2026-01-26T15:05:04Z","permalink":"https://iamxurulin.github.io/p/jdk%E7%89%88%E6%9C%AC%E7%9A%84%E5%8C%BA%E5%88%AB/","title":"JDK版本的区别"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void merge(int A[], int m, int B[], int n) { int i = m - 1; // 指针i：指向A有效元素的最后一位 int j = n - 1; // 指针j：指向B有效元素的最后一位 int k = m + n - 1; // 指针k：指向A物理空间的最后一位（合并后元素的存放位置） // 同时遍历A、B的有效元素，取较大值放到A的k位置 while (i \u0026gt;= 0 \u0026amp;\u0026amp; j \u0026gt;= 0) { if (A[i] \u0026gt; B[j]) { A[k--] = A[i--]; } else { A[k--] = B[j--]; } } // 若A的有效元素先遍历完，将B剩余元素依次放入A的剩余位置 if (i \u0026lt; 0) { while (j \u0026gt;= 0) { A[k--] = B[j--]; } } } ","date":"2026-01-26T04:15:00Z","permalink":"https://iamxurulin.github.io/p/%E5%8F%8C%E6%8C%87%E9%92%88%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84/","title":"【双指针】合并两个有序数组"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public String solve(String s, String t) { // 如果第一个字符串为空/长度为0，直接返回第二个字符串 if(s.length()\u0026lt;=0){ return t; } // 如果第二个字符串为空/长度为0，直接返回第一个字符串 if(t.length()\u0026lt;=0){ return s; } int i = s.length()-1; int j = t.length()-1; // 定义进位变量tmp int tmp = 0; // 定义StringBuilder拼接结果 StringBuilder sb = new StringBuilder(); // 需要注意：最后一位相加仍有进位时，需把进位1也拼接到结果 while (i\u0026gt;=0||j\u0026gt;=0||tmp!=0) { tmp += i\u0026gt;=0?s.charAt(i--)-\u0026#39;0\u0026#39;:0; tmp += j\u0026gt;=0?t.charAt(j--)-\u0026#39;0\u0026#39;:0; // 取余10：得到当前位的计算结果 sb.append(tmp%10); // 除以10：更新进位值 tmp=tmp/10; } // 结果逆序：因为是从个位开始拼接，需要反转回正序，再转字符串返回 return sb.reverse().toString(); } ","date":"2026-01-25T22:48:06Z","permalink":"https://iamxurulin.github.io/p/%E5%AD%97%E8%8A%82%E9%9D%A2%E8%AF%95%E6%89%8B%E6%92%95%E5%A4%A7%E6%95%B0%E5%8A%A0%E6%B3%95/","title":"【字节面试手撕】大数加法"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 public String solve(String IP) { // 校验是否为IPv4 if (IP.indexOf(\u0026#34;.\u0026#34;) != -1) { // 按.分割，.是正则通配符，需转义为\\\\. String[] strs = IP.split(\u0026#34;\\\\.\u0026#34;); // IPv4核心规则：必须被.分割为4段 if (strs.length != 4) { return \u0026#34;Neither\u0026#34;; } // 校验IPv4 for (int i = 0; i \u0026lt; strs.length; i++) { // 提前校验段非空，避免空串导致charAt/parseInt运行时异常 if (strs[i].isEmpty()) { return \u0026#34;Neither\u0026#34;; } // 当前段仅能包含数字0-9 for (char c : strs[i].toCharArray()) { if (!(c \u0026gt;= \u0026#39;0\u0026#39; \u0026amp;\u0026amp; c \u0026lt;= \u0026#39;9\u0026#39;)) { return \u0026#34;Neither\u0026#34;; } } // 数值≤255 且 无前导零（唯一例外：段本身是0，即长度1的0） if (Integer.parseInt(strs[i]) \u0026gt; 255 || (strs[i].charAt(0) == \u0026#39;0\u0026#39; \u0026amp;\u0026amp; strs[i].length() \u0026gt; 1)) { return \u0026#34;Neither\u0026#34;; } } // 所有IPv4规则校验通过 return \u0026#34;IPv4\u0026#34;; } // 校验是否为IPv6 else if (IP.indexOf(\u0026#34;:\u0026#34;) != -1) { // IPv6首尾不能为: if (IP.charAt(0) == \u0026#39;:\u0026#39; || IP.charAt(IP.length() - 1) == \u0026#39;:\u0026#39;) { return \u0026#34;Neither\u0026#34;; } // 按:分割IPv6字符串 String[] strs = IP.split(\u0026#34;:\u0026#34;); // IPv6必须被:分割为8段 if (strs.length != 8) { return \u0026#34;Neither\u0026#34;; } // 校验IPv6 for (int i = 0; i \u0026lt; strs.length; i++) { // 当前段仅能包含十六进制字符 for (char c : strs[i].toCharArray()) { if (!(c \u0026gt;= \u0026#39;0\u0026#39; \u0026amp;\u0026amp; c \u0026lt;= \u0026#39;9\u0026#39;) \u0026amp;\u0026amp; !(c \u0026gt;= \u0026#39;a\u0026#39; \u0026amp;\u0026amp; c \u0026lt;= \u0026#39;f\u0026#39;) \u0026amp;\u0026amp; !(c \u0026gt;= \u0026#39;A\u0026#39; \u0026amp;\u0026amp; c \u0026lt;= \u0026#39;F\u0026#39;)) { return \u0026#34;Neither\u0026#34;; } } // 段不能为空 且 长度≤4 if (strs[i].equals(\u0026#34;\u0026#34;) || strs[i].length() \u0026gt; 4) { return \u0026#34;Neither\u0026#34;; } } // 所有IPv6规则校验通过 return \u0026#34;IPv6\u0026#34;; } // 既不包含.也不包含:，直接判定为非法IP else { return \u0026#34;Neither\u0026#34;; } } 小贴士 题目意思不是太难理解，就是可能有点繁琐。 理清思路应该问题不大。\n","date":"2026-01-25T22:00:54Z","permalink":"https://iamxurulin.github.io/p/%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%88%86%E5%89%B2%E9%AA%8C%E8%AF%81ip%E5%9C%B0%E5%9D%80/","title":"【字符串分割】验证IP地址"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public String longestCommonPrefix (String[] strs) { if(strs==null||strs.length==0){ return \u0026#34;\u0026#34;; } int rows = strs.length; int cols = strs[0].length(); for(int i=0;i\u0026lt;cols;i++){ char c = strs[0].charAt(i); for(int j=1;j\u0026lt;rows;j++){ if(strs[j].length()==i||strs[j].charAt(i)!=c){ return strs[0].substring(0, i); } } } return strs[0]; } 小贴士 纵向扫描的思路大概是：\n以I【字符串数组】的第一个字符串的字符位置为 “列”，以i为列索引遍历每一列；\n取第一个字符串的当前列字符c作为基准，以j为行索引 遍历【字符串数组】的其他所有字符串，对比同列的字符。\n如果某个字符串长度等于当前列索引了，说明这个字符串已经没有更多字符了，\n或者同列字符不一致，\n那就说明公共前缀到i-1列就结束了，\n可直接返回strs[0].substring(0,i)；\n如果所有列都对比通过，说明第一个字符串就是最长公共前缀，直接返回strs[0]。\n","date":"2026-01-25T20:27:14Z","permalink":"https://iamxurulin.github.io/p/%E7%BA%B5%E5%90%91%E6%89%AB%E6%8F%8F%E6%9C%80%E9%95%BF%E5%85%AC%E5%85%B1%E5%89%8D%E7%BC%80/","title":"【纵向扫描】最长公共前缀"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public String trans(String s, int n) { if (s == null || s.length() == 0) { return s; } char[] str = s.toCharArray(); reverse(str, 0, str.length - 1); int i = 0, j = 0; while (i \u0026lt; str.length) { j = i; while (j \u0026lt; str.length \u0026amp;\u0026amp; str[j] != \u0026#39; \u0026#39;) { if (str[j] \u0026gt;= \u0026#39;a\u0026#39; \u0026amp;\u0026amp; str[j] \u0026lt;= \u0026#39;z\u0026#39;) { str[j] = (char) (str[j] - \u0026#39;a\u0026#39; + \u0026#39;A\u0026#39;); } else { str[j] = (char) (str[j] - \u0026#39;A\u0026#39; + \u0026#39;a\u0026#39;); } j++; } reverse(str, i, j - 1); i = j + 1; } return new String(str); } public void reverse(char[] arr, int start, int end) { int i = start; int j = end; while (i \u0026lt; j) { char c = arr[i]; arr[i] = arr[j]; arr[j] = c; i++; j--; } } 小贴士 代码思路大概是：\n整体反转➡️单词遍历➡️大小写互换➕局部反转\n","date":"2026-01-25T19:30:16Z","permalink":"https://iamxurulin.github.io/p/%E5%8F%8C%E6%8C%87%E9%92%88-%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8F%98%E5%BD%A2/","title":"【双指针+字符串】字符串变形"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public int maxProfit (int[] prices) { if(prices==null||prices.length\u0026lt;2){ return 0; } int n = prices.length; int[][] dp = new int[n][2]; dp[0][0]=0; dp[0][1]=-prices[0]; for(int i=1;i\u0026lt;n;i++){ dp[i][0]=Math.max(dp[i-1][0], dp[i-1][1]+prices[i]); dp[i][1]=Math.max(dp[i-1][1],-prices[i]); } return dp[n-1][0]; } 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public int maxProfit(int[] prices) { if ( prices == null || prices.length \u0026lt; 2 ) { return 0; } int n = prices.length; int[][] dp = new int[n][2]; dp[0][0] = 0; dp[0][1] = -prices[0]; for (int i = 1; i \u0026lt; n; i++) { dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]); } return dp[n - 1][0]; } 小贴士 和上面的类型（一）不一样的是，这题可以进行多次交易，代码上的体现就是：\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public int maxProfit(int[] prices) { return maxProfit_k(prices, 2); } public int maxProfit_k(int[] prices, int k) { if (prices == null || prices.length \u0026lt; 2) { return 0; } int n = prices.length; int[][][] dp = new int[n][k + 1][2]; for (int j = 0; j \u0026lt;= k; j++) { dp[0][j][0] = 0; dp[0][j][1] = -prices[0]; } for (int i = 1; i \u0026lt; n; i++) { for (int j = 1; j \u0026lt;= k; j++) { dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]); dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]); } } return dp[n - 1][k][0]; } 小贴士 说说三维DP的定义：\ndp[i][j][0]：第i天，最多完成了j次交易，不持有股票的最大利润；\ndp[i][j][1]：第i天，最多完成了j次交易，持有股票的最大利润；\n","date":"2026-01-25T00:04:30Z","permalink":"https://iamxurulin.github.io/p/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92-%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E5%A5%BD%E6%97%B6%E6%9C%BAi-ii-iii/","title":"动态规划-买卖股票的最好时机I-II-III"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public int rob (int[] nums){ int[] memo = new int[nums.length+1]; Arrays.fill(memo, -1); return dp(nums,0, memo); } private int dp(int[] nums,int start,int[] memo){ if(start\u0026gt;=nums.length){ return 0; } if(memo[start]!=-1){ return memo[start]; } int ans = Math.max(dp(nums, start+1,memo),nums[start]+dp(nums, start+2,memo)); memo[start]= ans; return ans; } 小贴士 记忆化存储+自顶向下\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public int rob(int[] nums) { int n = nums.length; int[] memo1 = new int[n + 1]; int[] memo2 = new int[n + 1]; Arrays.fill(memo1, -1); Arrays.fill(memo2, -1); int case1 = dp(nums, 0, n - 2, memo1); int case2 = dp(nums, 1, n - 1, memo2); return Math.max(case1, case2); } private int dp(int[] nums, int start, int end, int[] memo) { if (start \u0026gt; end) { return 0; } if (memo[start] != -1) { return memo[start]; } int ans = Math.max(dp(nums, start + 1, end, memo), nums[start] + dp(nums, start + 2, end, memo)); memo[start] = ans; return ans; } 小贴士 现在的情况是第一房间和最后一个房间也相当于是相邻的，不能同时抢。\n那么，首尾房间不能同时被抢，只可能有三种不同情况：\n都不被抢\n第一个房间被抢最后一间不抢\n最后一个房间被抢第一间不抢\n不过，本题不需要三种情况都进行比较，因为房间里的钱数都是非负数，后两种情况对于房间的选择余地肯定比情况一大，所以只要比较情况二和情况三就行了。\n代码也可以在【打家劫舍I】的【自顶向下+记忆化存储】基础上进行修改。\n需要注意的是独立的子问题必须使用独立的记忆化缓存，不能共用，不然会造成缓存被覆盖的问题，所以这里需要使用两个memory。\n","date":"2026-01-24T23:49:36Z","permalink":"https://iamxurulin.github.io/p/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92-%E6%89%93%E5%AE%B6%E5%8A%AB%E8%88%8Di-ii/","title":"动态规划-打家劫舍I-II"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 public int longestValidParentheses(String s) { char[] str = s.toCharArray(); int[] dp = new int[str.length]; int ans = 0; for (int i = 1, p; i = 0 \u0026amp;\u0026amp; str[p] == \u0026#39;(\u0026#39;) { dp[i] = dp[i - 1] + 2 + (p - 1 \u0026gt;= 0 ? dp[p - 1] : 0); } } ans = Math.max(ans, dp[i]); } return ans; } 小贴士 dp[i]是以字符串中第i个字符str[i]结尾的最长有效括号子串的长度。\n因为有效括号子串一定是以)结尾到，以(结尾的子串不可能有效。\n所以，只需要处理str[i] == ')'的情况，str[i] == '('时，让dp [i] = 0 即可。\n根据定义dp[i-1]表示的是以str[i-1]结尾的最长有效括号长度， 也就是说从i-dp[i-1]到i-1的这段子串是有效括号。\n那么，要匹配当前的这个str[i] = ')'，左括号必须出现在从i-dp[i-1]到i-1的这段有效子串的前一个位置，这个位置就是p，即i - dp[i-1] - 1。\np指的是当前)能形成有效匹配的唯一可能的左括号位置。\n状态转移方程的含义是：\n以i结尾的最长有效长度 = 前一个位置(i-1)的有效长度 + 当前匹配的一对括号 + 左括号位置(p)左边（如果有）的有效长度。\n","date":"2026-01-24T20:48:47Z","permalink":"https://iamxurulin.github.io/p/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92-%E6%9C%80%E9%95%BF%E7%9A%84%E6%8B%AC%E5%8F%B7%E5%AD%90%E4%B8%B2/","title":"动态规划-最长的括号子串"},{"content":"MySQL有binlog、redo log和undo log三种日志，其中binlog 负责主从复制和数据恢复，redo log保证崩溃后数据不丢失，undo log支撑事务回滚和MVCC。\n1.binlog binlog 是server层的日志，记录的是逻辑操作，也就是原始SQL或者行变更前后的值。\n它的核心场景是主从同步，从库拉取主库到binlog重放一遍就能保持数据一致。\n另外，做数据恢复的时候，也是靠binlog配合全量备份回放到指定时间点。\n2.redo log是InnoDB引擎独有的，记录的是物理变更，具体就是“某个数据页的某个偏移量改成了什么值”。 它的作用是崩溃恢复（crash-safe），基于WAL（Write Ahead Log）机制，MySQL挂了重启后，InnoDB会用redo log把没来得及刷盘的脏页恢复出来。\nredo log是循环写的，空间固定，写满了就得等checkpoint推进才能继续。\n3.undo log也是InnoDB引擎的逻辑日志，记录的是数据修改前的旧值。 事务回滚的时候，就靠undo log把数据改回去，另外MVCC的快照读也依赖它，别的事务要读历史版本，顺着undo log链往前找就行。\n","date":"2026-01-24T15:38:05Z","permalink":"https://iamxurulin.github.io/p/mysql%E4%B8%AD%E6%9C%89%E5%93%AA%E4%BA%9B%E6%97%A5%E5%BF%97%E7%B1%BB%E5%9E%8B/","title":"MySQL中有哪些日志类型？"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public boolean match (String str, String pattern) { int m = str.length(); int n = pattern.length(); boolean[][] dp = new boolean[m+1][n+1]; for(int i=0;i0\u0026amp;\u0026amp;(str.charAt(i-1)==pattern.charAt(j-1)||pattern.charAt(j-1)==\u0026#39;.\u0026#39;)){ dp[i][j]=dp[i-1][j-1]; } }else{ if(j\u0026gt;=2){ dp[i][j]|=dp[i][j-2]; } if(i\u0026gt;=1\u0026amp;\u0026amp;j\u0026gt;=2\u0026amp;\u0026amp;(str.charAt(i-1)==pattern.charAt(j-2)||pattern.charAt(j-2)==\u0026#39;.\u0026#39;)){ dp[i][j]|=dp[i-1][j]; } } } } } return dp[m][n]; } 小贴士 DP这个boolean数组，dp[i][j]的意思是str的前i个字符和pattern的前j个字符，能否匹配成功。\n这里的DP数组，考虑了下标0位置的空字符串，所以为了存储整个字符串，DP数组长度需要+1。\n这里没有像编辑距离那题一样单独处理初始化逻辑，而是在嵌套for循环的逻辑中处理的，这样代码会更简洁一些。\n这里有个细节需要注意一下就是，dp[i][j]是前i/j个字符，所以pattern的第j个字符对应原字符串的下标为j-1的字符，其实和【编辑距离】的下标转换是一样的。\n当pattern非空的时候，大范围是考虑了pattern[j-1]是否为*两种情况。\n1.当pattern[j-1]不是*时，如果str非空并且和str[j-1]相对或者pattern[j-1]直接是.的话，说明匹配成功。\n2.当pattern[j-1]是*的话，就需要考虑是零次匹配前面的字符还是匹配多次前面的字符的问题了。\n因为pattern[j-1]是*，所以pattern[j-2]就是*的前驱字符，那处理这类情况就应该着重看看pattern[j-2]了。\n如果是匹配0次前驱字符，那pattern的j-2和j-1位都可以舍弃，此时要判断str前i个和pattern前j个是否匹配，等价于判断str前i个和pattern前j-2个是否匹配。\n如果是匹配了多次前驱字符，那就让pattern保持不动，让str的前i-1个字符和pattern的前j个字符继续匹配。\n","date":"2026-01-23T23:42:18Z","permalink":"https://iamxurulin.github.io/p/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92-%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E5%8C%B9%E9%85%8D/","title":"动态规划-正则表达式匹配"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public int editDistance(String str1, String str2) { int m = str1.length(); int n = str2.length(); int[][] dp = new int[m + 1][n + 1]; for (int i = 0; i \u0026lt;= m; i++) { dp[i][0] = i; } for (int j = 0; j \u0026lt;= n; j++) { dp[0][j] = j; } for (int i = 1; i \u0026lt;= m; i++) { for (int j = 1; j \u0026lt;= n; j++) { if (str1.charAt(i - 1) == str2.charAt(j - 1)) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1; } } } return dp[m][n]; } 小贴士 1.dp[i][j]的意思是将str1的前i个字符转换为str2的前j个字符，所需要的最小编辑距离。 至于为什么要加1，是因为这里考虑了下标为0位置代表的空字符串，也就是DP的边界条件。 单独拎出来的那两个for循环，处理的是把空字符串转成str2的前j个字符需要j次插入操作，以及把str1的前i个字符转成空字符串需要i次删除操作。\n2.下面的两个for循环嵌套，是把str1的前m个字符转成str2的前n个字符，这里charAt方法是i-1主要是因为第i个字符对应的下标是i-1。\n","date":"2026-01-23T19:50:03Z","permalink":"https://iamxurulin.github.io/p/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92bm75-%E7%BC%96%E8%BE%91%E8%B7%9D%E7%A6%BB%E4%B8%80/","title":"【动态规划】BM75 编辑距离(一)"},{"content":"MySQL实现事务靠的是4个核心组件：\nRedo Log\nUndo Log\n锁\nMVCC\n这四个组件分别对应事务的ACID特性的不同方面，一致性是由原子性、隔离性、持久性共同作用实现的。\n1.Redo Log保证持久性（D）\n在事务提交时，基于WAL（预写日志）机制将修改先写到Redo Log再刷盘写磁盘数据页。\n就算写数据页时宕机了，重启后通过重放redo log就能恢复数据，确保已提交的修改不会丢失。\n2.Undo Log保证原子性（A）\n每次修改数据前，先把原值以逻辑日志的形式存到undo log中。\n事务回滚时，按undo log反向操作把数据恢复回去。\n要么全做完，要么全撤销，不会出现改了一半的中间状态。\n3.锁机制保证隔离性（I）的写-写并发\n两个事务同时改同一行，必须一个等另一个释放锁，实现写-写互斥。\nInnoDB的锁粒度精确到行级（基于索引实现），还有间隙锁防止幻读。\n4.MVCC保证隔离性（I）的读-写并发\n读操作不加锁，通过undo log构建的版本链找到自己应该看到的数据版本，主要适配InnoDB默认的可重复读隔离级别RR。\n写的时候别人照样能读，读的时候别人照样能写，大幅度提升读写并发度。\n一致性不是单独实现的，它是原子性、隔离性和持久性共同作用的结果。\n数据从一个正确状态转移到另一个正确状态，中间不会出现不一致的中间态，同时满足业务的约束规则。\n","date":"2026-01-23T15:59:01Z","permalink":"https://iamxurulin.github.io/p/mysql%E6%98%AF%E6%80%8E%E4%B9%88%E5%AE%9E%E7%8E%B0%E4%BA%8B%E5%8A%A1%E7%9A%84/","title":"MySQL是怎么实现事务的？"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 ArrayList\u0026lt;String\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); public ArrayList\u0026lt;String\u0026gt; restoreIpAddresses (String s) { if(s==null||s.length()\u0026lt;4||s.length()\u0026gt;12){ return ans; } StringBuilder sb = new StringBuilder(); dfs(s,sb,0,0); return ans; } private void dfs(String s,StringBuilder sb,int step,int index){ if(step==4){ if(index==s.length()){ ans.add(sb.toString()); } return; }else{ for(int i=index;i\u0026lt;index+3\u0026amp;\u0026amp;i\u0026lt;s.length();i++){ String cur = s.substring(index,i+1); if(Integer.parseInt(cur)\u0026gt;255||(cur.length()\u0026gt;1\u0026amp;\u0026amp;cur.charAt(0)==\u0026#39;0\u0026#39;)){ continue; } sb.append(cur); if(step\u0026lt;3){ sb.append(\u0026#39;.\u0026#39;); } dfs(s,sb,step+1,i+1); if(step\u0026lt;3){ sb.deleteCharAt(sb.length()-1); } sb.delete(sb.length()-cur.length(), sb.length()); } } } 小贴士 sb.delete(sb.length()-cur.length(), sb.length())这行代码是删除字符串中「从sb.length()-cur.length()索引（包含）到sb.length()索引（不包含）」的所有字符，是一个左闭右开区间。\n说人话就是：\n删除 StringBuilder 中最后面的、长度等于 cur 的所有字符，也就是是把之前拼接的当前 IP 段 cur 从 sb 中删掉，恢复 sb 到拼接 cur 前的状态。\n","date":"2026-01-23T00:25:31Z","permalink":"https://iamxurulin.github.io/p/dfs-%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%88%86%E5%89%B2-%E6%95%B0%E5%AD%97%E5%AD%97%E7%AC%A6%E4%B8%B2%E8%BD%AC%E5%8C%96%E6%88%90ip%E5%9C%B0%E5%9D%80/","title":"DFS-字符串分割-数字字符串转化成IP地址"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public int getLongestPalindrome (String A) { if(A==null||A.length()==0){ return 0; } int n = A.length(); char[] str = A.toCharArray(); int max = 1; boolean[][] dp = new boolean[n][n]; for(int i=0;i\u0026lt;n;i++){ dp[i][i]=true; } for(int i=1;i\u0026lt;n;i++){ for(int j=0;j\u0026lt;i;j++){ if(str[i]!=str[j]){ dp[j][i]=false; }else{ if(i-j\u0026lt;=1){ dp[j][i]=true; }else{ dp[j][i]=dp[j+1][i-1]; } if(dp[j][i]){ max=Math.max(max, i-j+1); } } } } return max; } 小贴士 1.dp[j][i] 表示子串 A[j..i]（闭区间）是否为回文，必须保证 j ≤ i\n判断规则： 首尾不等 → 非回文 首尾相等 + 长度≤2 → 必回文 首尾相等 + 长度 \u0026gt; 2 → 看中间子串是否回文 ","date":"2026-01-22T21:06:15Z","permalink":"https://iamxurulin.github.io/p/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92-%E6%9C%80%E9%95%BF%E5%9B%9E%E6%96%87%E5%AD%90%E4%B8%B2/","title":"动态规划-最长回文子串"},{"content":"虚拟内存的核心作用：\n● 扩展程序地址空间\n● 隔离进程地址空间\n● 简化内存管理\n虚拟内存允许程序运行在比实际物理内存更大的地址空间上。即使物理内存不足，系统也可以将不常用的页面移到磁盘的交换区，释放内存给活跃的程序使用，避免程序因内存不足而终止。\n操作系统通过页面表为每个内存页面设置访问权限，并由MMU在地址转换时校验权限。如果程序尝试非法访问，OS会拦截并触发异常，防止破坏其他程序或系统内核。\n虚拟内存将程序的逻辑地址与物理地址分离，一方面可以让程序员无需关心物理内存布局，简化开发；另一方面可以让进程拥有独立的虚拟地址空间，进程间无法直接访问，实现地址隔离，避免相互干扰。\n","date":"2026-01-22T19:18:56Z","permalink":"https://iamxurulin.github.io/p/%E8%A7%A3%E9%87%8A%E4%B8%80%E4%B8%8B%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E6%9C%89%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98/","title":"解释一下为什么要有虚拟内存"},{"content":"服务降级是一种在分布式系统和微服务架构中常用的容错机制。\n在系统压力过大、部分服务出现故障或计划性维护时，暂时减少或者关闭某些非核心或者低优先级的功能，从而确保核心功能的正常运行，避免系统崩溃。\n通过降级可以提高系统的容错性和可用性，保障核心业务的服务等级。\n服务降级的触发场景 1.当某个服务的调用时间超过了设定的阈值或者服务多次调用失败时，如果触发降级机制，返回预设的降级响应，比如默认值、友好提示，可以避免长时间的等待，提升用户体验。\n2.当系统的负载过高，比如CPU使用率、内存占用率、QPS达到阈值时，可以主动降级某些非核心的功能，释放系统资源，确保核心业务的正常运行。\n3.如果下游依赖服务不可用或者响应时间过长，可以通过降级机制，返回缓存数据或者默认数据，避免请求继续传播，阻断故障链，减少影响范围。\n4.在系统大促预热、底层依赖升级、预发布等场景下，可以主动提前降级非核心功能，提前释放资源，从而规避峰值压力导致的被动故障。\n","date":"2026-01-22T15:34:00Z","permalink":"https://iamxurulin.github.io/p/%E4%BB%80%E4%B9%88%E6%98%AF%E6%9C%8D%E5%8A%A1%E9%99%8D%E7%BA%A7/","title":"什么是服务降级？"},{"content":"MyBatis Flex和MyBatis Plus都是对原生MyBatis 框架的增强工具，都能够简化数据库的操作、提高开发效率。\n两者在设计理念和功能侧重点上有以下区别：\n1.MyBatis Flex除了MyBatis自身，没有任何第三方依赖，极致轻量化；而MyBatis Plus整合的功能更多（如逻辑删除、乐观锁、代码生成等），依赖也更复杂。\n2.MyBatis Flex从架构上进行了优化，在SQL执行的过程中没有SQL解析环节和MyBatis拦截器，通过 AST抽象语法树构建SQL，执行链路更短，可以带来更高的性能。而MyBatis Plus大量依赖MyBatis拦截器解析SQL，执行链路更长，有一定解析开销。\n3.MyBatis Flex原生支持多表关联查询，无需手写SQL，适合处理复杂的业务场景；而MyBatis Plus在多表查询时需要依赖第三方插件或者手写SQL来实现。\n","date":"2026-01-22T15:00:00Z","permalink":"https://iamxurulin.github.io/p/mybatis-flex%E5%92%8Cmybatis-plus%E7%9A%84%E5%8C%BA%E5%88%AB/","title":"MyBatis Flex和MyBatis Plus的区别"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 public int FindGreatestSumOfSubArray(int[] array) { int sum = 0; int max = array[0]; for(int i=0;i\u0026lt;array.length;i++){ sum= Math.max(array[i],sum+array[i]); max=Math.max(max, sum); } return max; } 小贴士 这题和前文【动态规划】最长上升子序列（一）有些类似，不同的是本题是连续子数组，常规思路的话我们需要利用dp，dp[i] 代表示以元素 array[i] 为结尾的连续子数组最大和。\n不难想到，状态转移方程： dp[i] = Math.max(dp[i-1]+array[i], array[i])\n这里我们为了进一步简化动态规划，使用一个变量sum来表示当前连续的子数组和。\n","date":"2026-01-21T21:15:37Z","permalink":"https://iamxurulin.github.io/p/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E8%BF%9E%E7%BB%AD%E5%AD%90%E6%95%B0%E7%BB%84%E7%9A%84%E6%9C%80%E5%A4%A7%E5%92%8C/","title":"【动态规划】连续子数组的最大和"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public int LIS(int[] arr) { if(arr==null||arr.length==0){ return 0; } int[] dp = new int[arr.length]; Arrays.fill(dp, 1); int ans = 1; for(int i=1;i\u0026lt;arr.length;i++){ for(int j=0;j\u0026lt;i;j++){ if(arr[i]\u0026gt;arr[j]){ dp[i]=Math.max(dp[j]+1, dp[i]); } } ans = Math.max(ans,dp[i]); } return ans; } 小贴士 状态转移：dp[i] = 前j个的最长长度+1 和 当前dp[i]的最大值\n","date":"2026-01-21T20:08:09Z","permalink":"https://iamxurulin.github.io/p/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%9C%80%E9%95%BF%E4%B8%8A%E5%8D%87%E5%AD%90%E5%BA%8F%E5%88%97%E4%B8%80/","title":"【动态规划】最长上升子序列（一）"},{"content":"1.首先主机应用程序启动并初始化MCP客户端，每个客户端与一个MCP服务器建立专属连接，确保通信链路的稳定性与安全性。\n2.在系统初始化阶段，MCP Client首先从MCP Server获取可用的工具清单和能力描述，让模型知道有哪些可以调用的外部技能。\n3.当用户输入一个问题后，MCP Client会将这些工具描述采用Function Calling格式传给LLM，让模型具备调用外部工具的能力。\n4.LLM根据当前对话的上下文、用户问题意图以及工具能力描述，判断是否需要使用外部工具，并决定调用哪一个工具、传入什么参数。\n5.如果模型发起调用请求，MCP Client会根据模型的选择，通过MCP Server发起标准化的工具调用请求，由MCP Server执行实际工具逻辑后，将结果返回给MCP Client。\n6.当工具的执行结果被传回LLM后，由模型将这个结果与原始用户问题以及已有的上下文等信息进行整合，生成符合用户需求的自然语言回应。\n7.最后，MCP Client将模型生成的回答展示给用户。\n","date":"2026-01-21T17:29:21Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4mcp%E7%9A%84%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B/","title":"说说MCP的工作流程"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public int minMoney (int[] arr, int aim) { int[] dp = new int[aim+1]; Arrays.fill(dp, Integer.MAX_VALUE); dp[0]=0; for(int i=0;i\u0026lt;arr.length;i++){ for(int j=arr[i];j\u0026lt;=aim;j++){ if(dp[j-arr[i]]!=Integer.MAX_VALUE){ dp[j]=Math.min(dp[j-arr[i]]+1,dp[j]); } } } return dp[aim]==Integer.MAX_VALUE?-1:dp[aim]; } 小贴士 dp[j] 表示的是凑出金额j所需的最少硬币数\n","date":"2026-01-20T23:37:19Z","permalink":"https://iamxurulin.github.io/p/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%85%91%E6%8D%A2%E9%9B%B6%E9%92%B1%E4%B8%80/","title":"【动态规划】兑换零钱（一）"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public int solve(String nums) { if (nums == null || nums.length() == 0 || nums.charAt(0) == \u0026#39;0\u0026#39;) { return 0; } int n = nums.length(); int prevPrev = 1; int prev = 1; for (int i = 2; i \u0026lt;= n; i++) { int curr = 0; char currChar = nums.charAt(i - 1); char prevChar = nums.charAt(i - 2); if (currChar != \u0026#39;0\u0026#39;) { curr += prev; } int twoDigit = Integer.parseInt(nums.substring(i - 2, i)); if (twoDigit \u0026gt;= 10 \u0026amp;\u0026amp; twoDigit \u0026lt;= 26) { curr += prevPrev; } prevPrev = prev; prev = curr; } return prev; } 小贴士 变量 对应传统DP数组 通俗含义 prevPrev dp[i-2] 前i-2个字符的解码方式数 prev dp[i-1] 前i-1个字符的解码方式数 curr dp[i] 临时变量，存储「前i个字符」的解码方式数 int prevPrev = 1 对应dp[0]=1，也就是空字符串的解码方式数，没有实际解码意义，只为让组合解码的计算成立（类似于乘法中的 “1”）\n","date":"2026-01-20T22:24:51Z","permalink":"https://iamxurulin.github.io/p/%E8%BF%AD%E4%BB%A3-%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%8A%8A%E6%95%B0%E5%AD%97%E7%BF%BB%E8%AF%91%E6%88%90%E5%AD%97%E7%AC%A6%E4%B8%B2/","title":"【迭代+动态规划】把数字翻译成字符串"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public int minPathSum(int[][] matrix) { int m = matrix.length; int n = matrix[0].length; int[][] dp = new int[m][n]; dp[0][0] = matrix[0][0]; for (int i = 1; i \u0026lt; m; i++) { dp[i][0] = dp[i - 1][0] + matrix[i][0]; } for (int j = 1; j \u0026lt; n; j++) { dp[0][j] = dp[0][j - 1] + matrix[0][j]; } for (int i = 1; i \u0026lt; m; i++) { for (int j = 1; j \u0026lt; n; j++) { dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + matrix[i][j]; } } return dp[m - 1][n - 1]; } 小贴士 代码结构跟上一篇【至下而上+动态规划】不同路径的数目（一）是一样的。\ndp[i][j] 的定义：从起点 (0,0) 走到位置 (i,j) 的最小路径和。\n","date":"2026-01-20T21:07:31Z","permalink":"https://iamxurulin.github.io/p/%E4%BB%8E%E4%B8%8B%E8%87%B3%E4%B8%8A-%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E7%9F%A9%E9%98%B5%E7%9A%84%E6%9C%80%E5%B0%8F%E8%B7%AF%E5%BE%84%E5%92%8C/","title":"【从下至上+动态规划】矩阵的最小路径和"},{"content":"在MySQL中监控和优化慢SQL的核心是开启MySQL的慢查询日志，慢查询日志会把执行时间超过设定阈值的SQL语句自动记录下来，通过日志的分析与执行计划的校验实现SQL性能的精准优化。\n主要步骤：\n1.开启慢查询日志，设置合理的时间阈值，线上环境通常将阈值设定为1~2秒，确保执行耗时较长的慢SQL能够被精准捕获。\n2.定期对慢查询日志进行分析，用MySQL自带的分析工具mysqldumpslow或Percona Toolkit里的pt-query-digest工具，对日志中的SQL进行汇总统计，定位高频出现的慢SQL。\n3.对高频的慢SQL执行EXPLAIN命令分析其执行计划，检查是否走索引、扫描数据行数、是否存在文件排序等问题，再针对分析结果开展针对性优化。\n几个常见的慢SQL优化方向 1.查看EXPLAIN 结果的type字段，如果值为ALL，说明当前SQL是全表扫描；如果key列为空，就说明该SQL未使用索引，需优化索引配置。\n2.如果SELECT查询的字段没有包含在索引里，会触发回表查询，可以通过构建覆盖索引进行优化，把查询所需的字段都放进索引。\n3.查看EXPLAIN结果的Extra字段，如果是Using filesort，说明数据库执行了手动排序；如果是Using temporary，说明用了临时表；这两种情况都可以通过为对应字段添加索引来避免。\n4.避免用SELECT *查询所有字段，可以明确指定所需的字段；可以通过添加LIMIT子句控制单次查询返回的条数，从而减少数据处理压力。\n","date":"2026-01-20T18:04:18Z","permalink":"https://iamxurulin.github.io/p/%E5%A6%82%E4%BD%95%E5%9C%A8mysql%E4%B8%AD%E7%9B%91%E6%8E%A7%E5%92%8C%E4%BC%98%E5%8C%96%E6%85%A2sql/","title":"如何在MySQL中监控和优化慢SQL？"},{"content":"MCP协议的安全性设计主要是为了确保大模型系统在与外部工具和资源进行交互时的安全性和可靠性。\n主要包含用户同意和控制、隔离与沙箱机制、加密传输与来源验证这3个层面。\n1.所有模型对工具、资源和提示的访问请求都必须经过用户的授权，用户必须知道哪些数据是需要提供给大模型的，在授权之前应该了解每个工具的功能。\n2.MCP将实际工具调用封装在MCP Server内部，模型本身无法直接访问敏感数据；沙箱机制对工具调用的执行环境进行限制，防止恶意操作对系统的破坏。\n3.MCP内置的安全机制确保只有经过了验证的请求才能访问特定的资源，另外MCP协议还支持多种加密算法。\n","date":"2026-01-20T15:43:02Z","permalink":"https://iamxurulin.github.io/p/mcp%E5%8D%8F%E8%AE%AE%E7%9A%84%E5%AE%89%E5%85%A8%E6%80%A7%E8%AE%BE%E8%AE%A1%E5%8C%85%E5%90%AB%E5%93%AA%E4%BA%9B%E5%B1%82%E9%9D%A2/","title":"MCP协议的安全性设计包含哪些层面？"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public int uniquePaths(int m, int n) { int[][] dp = new int[m][n]; for (int i = 0; i \u0026lt; m; i++) { dp[i][0] = 1; } for (int j = 0; j \u0026lt; n; j++) { dp[0][j] = 1; } for (int i = 1; i \u0026lt; m; i++) { for (int j = 1; j \u0026lt; n; j++) { dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; } } return dp[m - 1][n - 1]; } 小贴士 因为只能从上面或左边走过来，所以递推公式是 dp[i][j]=dp[i-1][j]+dp[i][j-1]；\n同时，也不难得出第一行和第一列都是1的边界条件。\n","date":"2026-01-20T00:51:24Z","permalink":"https://iamxurulin.github.io/p/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92-%E4%B8%8D%E5%90%8C%E8%B7%AF%E5%BE%84%E7%9A%84%E6%95%B0%E7%9B%AE/","title":"动态规划-不同路径的数目"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public String LCS(String s1, String s2) { if(s1==null||s2==null||s1.length()==0||s2.length()==0){ return \u0026#34;-1\u0026#34;; } int m = s1.length(); int n = s2.length(); String[][] dp = new String[m+1][n+1]; // 把所有dp[0][j] 和 dp[i][0] 赋值为空字符串 \u0026#34;\u0026#34;，解决null问题 for(int i=0;i\u0026lt;=m;i++){ dp[i][0] = \u0026#34;\u0026#34;; } for(int j=0;j\u0026lt;=n;j++){ dp[0][j] = \u0026#34;\u0026#34;; } for(int i=1;i\u0026lt;=m;i++){ for(int j=1;j\u0026lt;=n;j++){ if(s1.charAt(i-1)==s2.charAt(j-1)){ dp[i][j]=dp[i-1][j-1]+s1.charAt(i-1); }else{ String temp1String = dp[i-1][j]; String temp2String = dp[i][j-1]; dp[i][j]=temp1String.length()\u0026gt;temp2String.length()?temp1String:temp2String; } } } String res = dp[m][n]; return res.length()\u0026gt;0 ? res : \u0026#34;-1\u0026#34;; } 小贴士 dp数组会取到边界m和n，所以定义长度时需要加1。\n","date":"2026-01-20T00:50:32Z","permalink":"https://iamxurulin.github.io/p/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92-%E5%AD%97%E7%AC%A6%E4%B8%B2-%E6%9C%80%E9%95%BF%E5%85%AC%E5%85%B1%E5%AD%90%E7%B3%BB%E5%88%97%E4%BA%8C/","title":"动态规划-字符串-最长公共子系列二"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public int minCostClimbingStairs(int[] cost) { int n = cost.length; int[] memo = new int[n+1]; Arrays.fill(memo, -1); return dp(cost,n, memo); } private int dp(int[] cost,int n,int[] memo){ if(n==0||n==1){ return 0; } if(memo[n]!=-1){ return memo[n]; } memo[n]=Math.min(dp(cost, n-1, memo)+cost[n-1],dp(cost, n-2, memo)+cost[n-2]); return memo[n]; } 小贴士 所有的钱，都花在落脚的那一刻；跳的过程，一分钱不花。\n","date":"2026-01-20T00:50:13Z","permalink":"https://iamxurulin.github.io/p/%E8%AE%B0%E5%BF%86%E5%8C%96-%E9%80%92%E5%BD%92-%E6%9C%80%E5%B0%8F%E8%8A%B1%E8%B4%B9%E7%88%AC%E6%A5%BC%E6%A2%AF/","title":"记忆化+递归-最小花费爬楼梯"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public String LCS(String str1, String str2) { if(str1==null||str2==null||str1.length()==0||str2.length()==0){ return null; } int m = str1.length(); int n = str2.length(); String[][] dp = new String[m+1][n+1]; String ans =\u0026#34;\u0026#34;; for(int i=0;i\u0026lt;=m;i++){ dp[i][0]=\u0026#34;\u0026#34;; } for(int j=0;j\u0026lt;=n;j++){ dp[0][j]=\u0026#34;\u0026#34;; } for(int i=1;i\u0026lt;=m;i++){ for(int j=1;j\u0026lt;=n;j++){ if(str1.charAt(i-1)==str2.charAt(j-1)){ dp[i][j]=dp[i-1][j-1]+str1.charAt(i-1); }else{ dp[i][j]=\u0026#34;\u0026#34;; } String tempString = dp[i][j]; ans = ans.length()\u0026gt;tempString.length()?ans:tempString; } } return ans.length()\u0026gt;0?ans:null; } 小贴士 1.这道题和前文最长公共子序列的代码基本一致，主要改变就是连续子串它不能断，所以不相等的时候直接置为空字符串。\n\u0026quot;\u0026quot; 是一个合法的、实实在在、有效的 String 类实例对象，只是这个对象里没有任何字符（字符长度为 0），对象内容为空；\nnull 表示一个 String 类型的变量，没有指向任何内存地址，没有绑定任何 String 对象，不占用任何内存空间，内存中完全没有这个对象的痕迹，是【悬空状态】\n","date":"2026-01-20T00:35:35Z","permalink":"https://iamxurulin.github.io/p/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92-%E6%9C%80%E9%95%BF%E5%85%AC%E5%85%B1%E5%AD%90%E4%B8%B2/","title":"动态规划-最长公共子串"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public int jumpFloor(int number) { int[] memo = new int[number + 1]; return dp(number, memo); } private int dp(int number,int[] memo){ if(number\u0026lt;=2){ return number; } if(memo[number]!=0){ return memo[number]; } memo[number]=dp(number-1, memo)+dp(number-2, memo); return memo[number]; } 小贴士 1.要跳到第 n 级，最后一步只有两种可能：从 n-1 级跳 1 级上来、从 n-2 级跳 2 级上来，总方法数就是两者之和。\n2.构建一个备忘录数组，用来缓存已经计算过的结果，数组下标对应台阶数，值对应该台阶的跳法数。\n由于数组的默认值为0，0就表示该台阶的跳法数还未计算；\n如果值≠0，就表示已经计算过，直接取值即可。\n","date":"2026-01-19T20:48:04Z","permalink":"https://iamxurulin.github.io/p/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E9%80%92%E5%BD%92-%E8%AE%B0%E5%BF%86%E5%8C%96%E5%AD%98%E5%82%A8-%E8%B7%B3%E5%8F%B0%E9%98%B6/","title":"动态规划=递归+记忆化存储-跳台阶"},{"content":"RPC（Remote Procdure Call）远程过程调用是一种用于实现在分布式系统中进行跨网络通信的技术，也是一种计算机通信协议。\nRPC框架是基于RPC协议实现的。\nRPC允许一个程序（服务消费者）像调用自己程序的方法一样，调用另一个程序（服务提供者）的接口，无需关心数据的传输处理、数据编解码和底层网络通信的细节。\n这样一种功能的实现都是由RPC框架来完成的，这就使得开发者能够更轻松地调用远程服务，快速开发分布式系统。\nRPC框架通常采用的是高效地网络通信协议（主流为性能远超HTTP的TCP协议）和高效轻量的序列化/反序列化机制；\nRPC框架提供的负载均衡、服务发现和容错机制（超时重试、熔断降级、限流等）可以提高系统的可靠性、可用性以及稳定性。\n除此之外，开发者还可以自定义负载均衡器和序列化协议，动态地扩展RPC的功能。\n","date":"2026-01-19T17:43:23Z","permalink":"https://iamxurulin.github.io/p/%E4%BB%80%E4%B9%88%E6%98%AFrpc%E6%A1%86%E6%9E%B6/","title":"什么是RPC框架"},{"content":"SQL调优的核心思路是减少磁盘I/O和避免无效计算。\n主要就是先通过MySQL的慢查询日志定位慢SQL，再利用EXPLAIN分析执行计划，最后再进行针对性优化。\n优化的手段主要有这几大类，分别是索引层面的优化，SQL写法层面的优化以及架构层面的优化。\n索引层面的优化 合理设计联合索引，利用覆盖索引来避免回表。\n注意最左匹配原则。\n避免在索引列上做函数运算、隐式类型转换。\n及时删除荣誉索引和重复索引。\nSQL写法层面的优化 为了减少网络传输和内存占用，只对必要字段进行查询，不使用select * 为了避免全表扫描，避免%LIKE这种前缀模糊查询 连表查询时检查关联字段的字符集和排序规则是否一致，不一致会导致索引失效 架构层面的优化 对于访问频率高但是变化少的数据使用Redis缓存 对单表数据量超过2000万行或物理文件超过2G的大表进行分库分表 搭建主从数据库集群，进行读写分离，让从库分担一些查询的压力 ","date":"2026-01-19T17:23:03Z","permalink":"https://iamxurulin.github.io/p/mysql%E4%B8%AD%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8Csql%E8%B0%83%E4%BC%98/","title":"MySQL中如何进行SQL调优"},{"content":"将已有的应用转换为MCP（Model Context Protocol）服务需要将该应用的功能封装为标准化的MCP工具、资源或者提示，再通过MCP Server对外暴露。\n主要步骤如下：\n1.首先需要识别应用中要提供给外部调用的功能，比如说API接口、数据查询、业务计算、资源检索这类能力。\n2.再创建一个新的MCP服务，将其与已有的业务服务隔离开，通过标准化的内部API或SDK和业务服务进行解耦通信。\n3.对MCP服务需要包含的工具功能描述、方法入参字段、类型、描述，以及方法返回值的字段、类型、描述进行标准化定义。\n4.根据MCP协议规范，构建一个用于接收MCP 客户端请求、做标准化协议解析与参数校验、调用相应业务功能并返回标准化结果的MCP Server。\n","date":"2026-01-19T16:38:18Z","permalink":"https://iamxurulin.github.io/p/%E6%80%8E%E4%B9%88%E5%B0%86%E5%B7%B2%E6%9C%89%E7%9A%84%E5%BA%94%E7%94%A8%E8%BD%AC%E6%8D%A2%E6%88%90mcp%E6%9C%8D%E5%8A%A1/","title":"怎么将已有的应用转换成MCP服务"},{"content":"大模型微调（Fine-tuning of Large Models）是指在预训练（Pre-training）模型的基础上，使用特定任务的数据对模型进行再训练，使模型适应特定应用场景的需求，本质上就是迁移学习在大模型中的落地方式。\n微调和预训练的区别主要在于目标、数据来源和训练方式。\n1.预训练通常是在大规模通用数据集上进行训练，让模型学习通用的语言规律或者知识；而微调通常是在特定任务的数据集上进行训练，让模型适应特定的任务。\n2.预训练通常采用的是无监督或者自监督学习的方式，而微调通常采用的是监督学习的方式。\n微调在自然语言处理中的文本分类、命名实体识别以及计算机视觉中的图像分类、目标检测都有广泛的应用。\n","date":"2026-01-19T16:12:13Z","permalink":"https://iamxurulin.github.io/p/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%9A%84%E5%BE%AE%E8%B0%83%E5%92%8C%E9%A2%84%E8%AE%AD%E7%BB%83%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88/","title":"大模型的微调和预训练区别是什么"},{"content":"任何通过Spring 容器实例化、组装和管理的Java对象都能称之为Spring Bean。\nBean可以看成是Spring应用中的一个普通Java对象，这个对象的创建、属性赋值、初始化、销毁等完整的生命周期是由Spring IOC容器来统一管理的，并非程序员手动控制。\nSpring Bean的生命周期分为实例化、依赖注入、初始化以及销毁这4个固定不可逆的阶段。\n当Spring 容器启动时，会根据配置文件或者注解，先实例化Bean；\n之后，Spring 容器再通过注解（@Autowired）或者setter方法将Bean的依赖属性注入进来；\n依赖注入完成后，如果Bean标注了@PostConstruct注解、实现了InitializingBean接口或者配置了init-method，Spring就会调用相应的初始化方法，完成Bean的初始化操作。\n初始化完成后，Bean就进入到了就绪状态，可以被程序获取和使用了。\n如果Bean标注了@PreDestroy注解、实现了DisposableBean接口或者配置了destroy-method，Spring会在容器关闭时调用对应的销毁方法，完成Bean的资源释放。\n","date":"2026-01-19T15:50:02Z","permalink":"https://iamxurulin.github.io/p/%E4%BB%80%E4%B9%88%E6%98%AFspring-bean/","title":"什么是Spring-Bean"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 public int Fibonacci(int n) { if(n==1||n==2){ return 1; } return Fibonacci(n-1)+Fibonacci(n-2); } ","date":"2026-01-19T04:30:00Z","permalink":"https://iamxurulin.github.io/p/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92-%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E6%95%B0%E5%88%97/","title":"动态规划-斐波那契数列"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 private ArrayList\u0026lt;String\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); private void backtrack(int left,int right,StringBuilder sb){ if(left==0\u0026amp;\u0026amp;right==0){ ans.add(new String(sb)); return; } if(left\u0026gt;right){ return; } if(left\u0026lt;0||right\u0026lt;0){ return; } sb.append(\u0026#39;(\u0026#39;); backtrack(left-1, right, sb); sb.deleteCharAt(sb.length()-1); sb.append(\u0026#39;)\u0026#39;); backtrack(left, right-1, sb); sb.deleteCharAt(sb.length()-1); } public ArrayList\u0026lt;String\u0026gt; generateParenthesis(int n) { StringBuilder sb = new StringBuilder(); backtrack(n, n, sb); return ans; } 小贴士 对于一个合法的括号字符串组合，其任意位置的左括号数量都大于等于右括号数量。\n","date":"2026-01-19T00:24:24Z","permalink":"https://iamxurulin.github.io/p/%E5%9B%9E%E6%BA%AF-%E6%8B%AC%E5%8F%B7%E7%94%9F%E6%88%90/","title":"回溯-括号生成"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 private int ans = 0; public int Nqueen(int n) { int[] pos = new int[n]; backtrack(n, 0, pos); return ans; } private boolean isValid(int[] pos,int row,int col){ for(int i=0;i\u0026lt;row;i++){ if(pos[i]==col||Math.abs(row-i)==Math.abs(col-pos[i])){ return false; } } return true; } private void backtrack(int n,int row,int[] pos){ if(row==n){ ans++; return; } for(int i=0;i\u0026lt;n;i++){ if(isValid(pos, row, i)){ pos[row]=i; backtrack(n, row+1, pos); } } } 小贴士 1.pos[i]==col||Math.abs(row-i)==Math.abs(col-pos[i]这个校验逻辑为什么是这样？\n主要是因为回溯是逐行递归，一行只放一个皇后，天然规避同行冲突，所以这里没有写i==row;\n然后利用了对角线的数学判定公式：\n两个皇后的【行号差值的绝对值】 = 【列号差值的绝对值】➡️必在同一条对角线上\n2.在 N 皇后问题中，行不会重复选（遍历规则）、列不会重复选（校验规则），used数组的使命已经被替代，所以不需要写 used 数组。\n3.本题的回溯为什么不需要显式写撤销代码？\n做选择时只修改了当前层级专属的变量，且变量会被下一次循环自动覆盖，覆盖之后，旧值就消失了，相当于恢复成了原始状态。\n","date":"2026-01-19T00:24:00Z","permalink":"https://iamxurulin.github.io/p/%E5%9B%9E%E6%BA%AF-n%E7%9A%87%E5%90%8E%E9%97%AE%E9%A2%98/","title":"回溯-N皇后问题"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 private int m, n; private int dfs(int[][] matrix, int[][] dp, int i, int j) { // 记忆化：已经计算过该位置的最长路径，直接返回 if (dp[i][j] != 0) { return dp[i][j]; } dp[i][j]++; if (i - 1 \u0026gt;= 0 \u0026amp;\u0026amp; matrix[i - 1][j] \u0026gt; matrix[i][j]) { dp[i][j] = Math.max(dp[i][j], dfs(matrix, dp, i - 1, j) + 1); } if (i + 1 \u0026lt; m \u0026amp;\u0026amp; matrix[i + 1][j] \u0026gt; matrix[i][j]) { dp[i][j] = Math.max(dp[i][j], dfs(matrix, dp, i + 1, j) + 1); } if (j - 1 \u0026gt;= 0 \u0026amp;\u0026amp; matrix[i][j - 1] \u0026gt; matrix[i][j]) { dp[i][j] = Math.max(dp[i][j], dfs(matrix, dp, i, j - 1) + 1); } if (j + 1 \u0026lt; n \u0026amp;\u0026amp; matrix[i][j + 1] \u0026gt; matrix[i][j]) { dp[i][j] = Math.max(dp[i][j], dfs(matrix, dp, i, j + 1) + 1); } return dp[i][j]; } public int solve(int[][] matrix) { if (matrix == null || matrix.length == 0 || matrix[0].length == 0) { return 0; } int ans = 0; m = matrix.length; n = matrix[0].length; int[][] dp = new int[m][n]; // 遍历矩阵的每一个单元格，作为递增路径的起点 for (int i = 0; i \u0026lt; m; i++) { for (int j = 0; j \u0026lt; n; j++) { ans = Math.max(ans, dfs(matrix, dp, i, j)); } } return ans; } 小贴士 dp[i][j]是以矩阵中第 i 行 j 列的单元格为起点的最长递增路径的长度。\n如果计算过dp[i][j]，就会把结果存入数组，后续再递归到这个单元格时，可直接返回结果。\n说说代码Math.max(dp[i][j], dfs(matrix, dp, i + 1, j) + 1)：\ndp[i][j]是当前单元格「已经算出的最长路径长度」；\ndfs(...) +1是当前单元格「走上下左右的某个方向能得到的路径长度」；\n","date":"2026-01-19T00:23:17Z","permalink":"https://iamxurulin.github.io/p/dfs-%E7%9F%A9%E9%98%B5%E6%9C%80%E9%95%BF%E9%80%92%E5%A2%9E%E8%B7%AF%E5%BE%84/","title":"DFS-矩阵最长递增路径"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 ArrayList\u0026lt;String\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); public ArrayList\u0026lt;String\u0026gt; Permutation(String str) { StringBuilder sb = new StringBuilder(); boolean[] used = new boolean[str.length()]; char[] chs = str.toCharArray(); Arrays.sort(chs); str = new String(chs); backtrack(str, sb, used); return ans; } private void backtrack(String str, StringBuilder sb, boolean[] used) { if (sb.length() == str.length()) { ans.add(new String(sb)); return; } for (int i = 0; i \u0026lt; str.length(); i++) { if (used[i]) { continue; } if (i \u0026gt; 0 \u0026amp;\u0026amp; str.charAt(i) == str.charAt(i - 1) \u0026amp;\u0026amp; !used[i - 1]) { continue; } sb.append(str.charAt(i)); used[i] = true; backtrack(str, sb, used); sb.deleteCharAt(sb.length() - 1); used[i] = false; } } 小贴士 这道题本质上跟【回溯】有重复项数字的全排列是一样的，都是【有重复项数字的全排列问题】，只不过从普通的数组环境迁移到了字符串环境，其实字符串本质上也是数组，字符数组嘛。\n","date":"2026-01-18T20:37:04Z","permalink":"https://iamxurulin.github.io/p/%E5%9B%9E%E6%BA%AF-%E5%89%AA%E6%9E%9D-%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E6%8E%92%E5%88%97/","title":"回溯+剪枝-字符串的排列"},{"content":"Spring IOC（Inversion of Control）控制反转是将对象的创建权和对象之间依赖关系的维护权，从程序员的业务代码中转移到由Spring IOC容器中，程序员就不再手动控制了。\n需要说明的是，Spring IOC是一种设计思想，不是一个具体的技术，Spring IOC 的具体落地实现是通过依赖注入（Dependency Injection） 完成的。\n依赖注入的方法有构造器注入、setter注入和接口注入三种。\n那控制反转它控制的是什么呢？\n其实就是控制的是对象的创建过程，Spring IOC容器会根据配置文件来创建对象。\n那反转的是什么呢？\n反转的是创建对象并且为这个要创建的对象注入它所依赖的对象这个动作的主动权。\n传统开发过程是由程序员主动new一个对象，手动注入依赖，Spring IOC是将这个动作交给容器来自动完成，这样主动权就发生了反转。\n举个🌰说明一下：\n比如对象A依赖对象B，那在创建对象A的代码里，我们就需要写好应该如何创建对象B，只有这样才能创建一个完整的对象A。\n但是，反转之后，这个动作就会由Spring IOC容器去触发，容器在创建对象A的时候，发现对象A它依赖对象B，根据配置文件，容器就会创建对象B，然后将对象B注入到对象A中。\n这里要注意一下，例子中的是注入一个对象，其实还可以注入配置文件中的一个值、集合等等。\n","date":"2026-01-18T18:12:06Z","permalink":"https://iamxurulin.github.io/p/%E4%BB%80%E4%B9%88%E6%98%AFspring-ioc/","title":"什么是Spring-IOC"},{"content":"反射机制是让程序在运行的时候能够动态地获取类的方法、字段、构造函数这些结构信息，还能对其直接进行操作，这样，程序在编译时不需要知道具体的类型，等到运行时再决定要调用哪个类和方法。\n反射机制主要有3大核心流程： 获取Class对象➡️获取成员信息➡️操作目标对象。\n获取Class对象的方式： 1.全限定类名获取 1 Class\u0026lt;?\u0026gt; cla = Class.forName(\u0026#34;xxx.xxx.MyClass\u0026#34;); 2.字面量获取 1 Class\u0026lt;?\u0026gt; cla = MyClass.class; 3.对象实例获取 1 Class\u0026lt;?\u0026gt; cla = obj.getClass(); 获取成员信息 主要是从Class对象中获取字段Field、方法Method以及构造函数Constructor\n操作目标对象 主要是创建实例、读写字段和调用方法。\n","date":"2026-01-18T17:36:55Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4java%E4%B8%AD%E7%9A%84%E5%8F%8D%E5%B0%84%E6%9C%BA%E5%88%B6/","title":"说说Java中的反射机制"},{"content":"0.75这个数字其实是一个时间和空间之间的平衡点。\n负载因子决定了什么时候会触发扩容。\nHashMap的扩容阈值=容量✖️负载因子。\n举个栗子：\n比如容量是16，负载因子是0.75，这样存到第12个元素的时候就会触发扩容；\n那如果负载因子改成0.5，这样存到第8个元素就会触发扩容；\n如果负载因子改成1，那这样只有存满16个元素才会触发扩容。\n所以，\n负载因子如果设置的太低，桶里的元素就会越稀疏，这样虽然查找速度会更快，但是会造成内存空间的浪费；\n负载因子如果设置的太高的话，桶里的元素就会越密集，虽然节省空间，但是冲突也会变多，这样查找就会变慢。\n0.75是一个折中的值，既不会让数组太过空旷，也不会让链表太长，在大多数场景下都能保持不错的性能。\n","date":"2026-01-18T17:11:19Z","permalink":"https://iamxurulin.github.io/p/java%E4%B8%ADhashmap%E7%9A%84%E9%BB%98%E8%AE%A4%E8%B4%9F%E8%BD%BD%E5%9B%A0%E5%AD%90%E4%B8%BA%E4%BB%80%E4%B9%88%E8%AE%BE%E7%BD%AE%E4%B8%BA0.75/","title":"Java中HashMap的默认负载因子为什么设置为0.75"},{"content":"PEFT（Parameter Efficient Fine Tuning）参数高效微调是一种在微调预训练大模型时，只对适配器模块、低秩增量矩阵和提示嵌入这些少量的新增参数进行训练，并对原始模型的大部分权重进行冻结的策略。\nPEFT能够在计算和存储成本大幅度降低的前提下，仍然能够保持与全量微调相近的性能。\n那什么要有这样一种微调方法呢？\n主要是全量微调对显存、算力以及存储的要求都非常高，并且迭代速度很慢，不利于多任务和在线更新；\n如果是在小样本场景下还容易出现过拟合问题。\n而我们的PEFT可以通过只更新数百万级别或者更少的参数，就能好实现高效微调和快速迭代。\nPEFT的主要优势是：\n1.PEFT方法能够将可训练参数比例控制在0.1%~5%之间，能够显著地节省计算资源和存储空间。\n2.少量的可训练参数往往意味着反向传播与梯度更新的计算量大幅度下降，训练时间也能够缩短数倍，这样就能加快训练的收敛速度。\n3.由于PEFT保留了预训练阶段学习到的通用知识，因此在数据稀缺或者小样本环境下，还能够降低过拟合的风险。\n4.PEFT生成的Adapter、LoRA和Prefix这些增量模块可以和基础模型分离存储，这样不同的任务只需要加载对应的模块。\n","date":"2026-01-18T16:51:50Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4%E5%A4%A7%E6%A8%A1%E5%9E%8B%E4%B8%AD%E7%9A%84peft/","title":"说说大模型中的PEFT"},{"content":"Fine-tuning大模型微调指的是在预训练模型的基础上，使用特定的数据对模型进行再训练，以适应特定应用场景的需求。\n常见的微调方法分为全量微调、参数高效微调以及量化与混合微调这三类。\n全量微调 全量微调是直接对预训练模型中的所有参数进行再训练，使模型适应特定任务或者领域的需求，这种方式可以获得最佳的任务性能，不过缺点也很明显，因为是对所有参数再次训练，所以计算和存储的开销极大。\n参数高效微调（Parameter-Efficient Fine-tuning，PEFT） 参数高效微调只更新少量的参数，从而大幅度降低训练和存储的成本。\n有以下6种方法：\nAdapter Tuning 是在Transformer`每层中插入小模块，只训练Adapter。\nLoRA（Low Rank Adaptation）为每层添加低秩分解矩阵，然后只对这些 矩阵进行训练，是目前最流行的一种PEFT方法。\nPrefix Tuning在输入前加入可训练的prefix tokens，模型本体权重完全冻结。\nPrompt Tuning只训练用于任务提示的嵌入向量，不改变模型的内部结构，以提示的形式引导模型的行为。\nP-Tuning通过可学习的伪提示目标来优化。\nBitFit只更新bias参数，训练参数占比通常小于0.1%。\n量化与混合微调 QLoRA结合了4-bit量化、LoRA、双量化和分页优化器，利用低精度存储与低秩适配，单卡就能对百亿级模型进行微调。\nIR -QLoRA在量化阶段引入信息保留机制与弹性联结，进一步减少了量化带来的性能损耗。\n","date":"2026-01-18T16:26:29Z","permalink":"https://iamxurulin.github.io/p/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E4%B8%AD%E5%B8%B8%E8%A7%81%E7%9A%84%E5%BE%AE%E8%B0%83%E6%96%B9%E6%B3%95/","title":"大模型中常见的微调方法"},{"content":" 这个错误是编辑器的 CSS 校验器 不认识 Hugo 模板语法导致的假报错。\n主要是在项目的 terms.html 文件中，有一段内联 CSS：\n1 2 3 CSSbackground: linear-gradient(135deg, {{ ... }} 0%, {{ ... }} 100%); 编辑器看到 {{ ... }} 这些内容时，以为这是普通的 CSS 代码，但实际上，这些 {{ }} 是 Hugo 的模板指令，这种假报错虽然不会影响站点运行，并且 hugo server 能正常启动、页面显示也没问题。\n不过，想消除也不是没有办法，且看：\n在项目的根目录下创建一个【.vscode】文件夹📂，然后创建一个【settings.json 】文件：\n1 2 3 4 5 { \u0026#34;css.validate\u0026#34;: false, \u0026#34;less.validate\u0026#34;: false, \u0026#34;scss.validate\u0026#34;: false } 之后错误❌就消失了： ","date":"2026-01-18T15:21:15Z","permalink":"https://iamxurulin.github.io/p/%E6%B6%88%E9%99%A4vs-code%E5%9C%A8%E6%A3%80%E6%9F%A5style%E6%A0%87%E7%AD%BE%E9%87%8C%E9%9D%A2%E7%9A%84-css-%E4%BB%A3%E7%A0%81%E6%97%B6%E4%BA%A7%E7%94%9F%E7%9A%84%E8%AF%AD%E6%B3%95%E8%AD%A6%E5%91%8A/","title":"消除VS-Code在检查style标签里面的-CSS-代码时产生的语法警告"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public int solve (char[][] grid) { int ans = 0; int m = grid.length,n =grid[0].length; for(int i=0;i\u0026lt;m;i++){ for(int j=0;j\u0026lt;n;j++){ if(grid[i][j]==\u0026#39;1\u0026#39;){ ans++; dfs(grid, i, j); } } } return ans; } private void dfs(char[][] grid,int i,int j){ if(i\u0026lt;0||j\u0026lt;0||i\u0026gt;=grid.length||j\u0026gt;=grid[0].length){ return; } if(grid[i][j]==\u0026#39;0\u0026#39;){ return; } grid[i][j]=\u0026#39;0\u0026#39;; dfs(grid, i-1, j); dfs(grid, i+1, j); dfs(grid, i, j-1); dfs(grid, i, j+1); } 小贴士 1.这道题，发现一块陆地 grid[i][j]='1' 时，立刻把这块陆地，以及和它相连的所有陆地，全部修改为海水 '0'，这样做可以实现用原数组本身做访问标记，这样就不需要额外创建 boolean[][] used 数组了。\n2.解释一下代码里是 看到1就ans++，为什么不会把同一块相连的 1 重复计数？\n其实，ans++ 并不是见到 1 就加，而是什么呢？\n是这样的，发现了一个全新的、独立的岛屿的起点，这个时候岛屿数量就会 + 1，我们还要注意紧跟的 dfs(grid,i,j) 。\n这个dfs会把岛屿的起点所属的一整块相连的所有 1，全部永久变成 0，这么做就能够实现一个岛屿，只会触发 1 次 ans++\n","date":"2026-01-18T05:00:00Z","permalink":"https://iamxurulin.github.io/p/dfs-%E5%B2%9B%E5%B1%BF%E6%95%B0%E9%87%8F/","title":"DFS-岛屿数量"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); public ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; permuteUnique (int[] num) { ArrayList\u0026lt;Integer\u0026gt; track = new ArrayList\u0026lt;\u0026gt;(); boolean[] used = new boolean[num.length]; Arrays.sort(num); backtrack(num, track, used); return ans; } private void backtrack(int[] num,ArrayList\u0026lt;Integer\u0026gt; track,boolean[] used){ if(track.size()==num.length){ ans.add(new ArrayList\u0026lt;\u0026gt;(track)); return; } for(int i=0;i\u0026lt;num.length;i++){ if(used[i]){ continue; } if(i\u0026gt;0\u0026amp;\u0026amp;num[i]==num[i-1]\u0026amp;\u0026amp;!used[i-1]){ continue; } track.add(num[i]); used[i]=true; backtrack(num, track, used); track.remove(track.size()-1); used[i]=false; } } 小贴士 在代码层面，这道【有重复项数字的全排列问题】和【没有重复项数字的全排列问题】不同的就是增加了一个排序语句和一个if语句。\n其中，排序是为了让重复元素相邻，后续只需要判断相邻的重复元素即可。\n那下面的if语句为什么是!used [i-1] 才跳过呢？\n这里可能有点绕，主要是因为：\n如果当前元素和前一个元素重复，且前一个元素没被使用，那么选当前元素的所有递归分支，和选前一个元素的递归分支，是完全一模一样的！\n直白一点就是，前一个重复元素都没选，你还选当前这个，纯属多此一举，还会制造重复，必须剪掉！\n","date":"2026-01-18T04:30:00Z","permalink":"https://iamxurulin.github.io/p/%E5%9B%9E%E6%BA%AF-%E6%9C%89%E9%87%8D%E5%A4%8D%E9%A1%B9%E6%95%B0%E5%AD%97%E7%9A%84%E5%85%A8%E6%8E%92%E5%88%97/","title":"回溯-有重复项数字的全排列"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); public ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; permute (int[] num) { ArrayList\u0026lt;Integer\u0026gt; track = new ArrayList\u0026lt;\u0026gt;(); boolean[] used = new boolean[num.length]; backtrack(num,track,used); return ans; } private void backtrack(int[] num,ArrayList\u0026lt;Integer\u0026gt; track,boolean[] used){ if(track.size()==num.length){ ans.add(new ArrayList\u0026lt;\u0026gt;(track)); return; } for(int i=0;i\u0026lt;num.length;i++){ if(used[i]){ continue; } track.add(num[i]); used[i]=true; backtrack(num,track,used); track.remove(track.size()-1); used[i]=false; } } 小贴士 回溯的核心是状态回退，也就是递归深入时修改的状态（比如本题的track、used），递归返回时必须恢复原状，只有这样才能保证下一轮循环的选择是干净的。\n","date":"2026-01-18T04:15:00Z","permalink":"https://iamxurulin.github.io/p/%E5%9B%9E%E6%BA%AF-%E6%B2%A1%E6%9C%89%E9%87%8D%E5%A4%8D%E9%A1%B9%E6%95%B0%E5%AD%97%E7%9A%84%E5%85%A8%E6%8E%92%E5%88%97/","title":"回溯-没有重复项数字的全排列"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; threeSum (int[] num) { ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; ans = new ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt;(); Arrays.sort(num); for(int i=0;i\u0026lt;num.length \u0026amp;\u0026amp; num[i]\u0026lt;=0;i++){ if(i\u0026gt;0\u0026amp;\u0026amp;num[i]==num[i-1]){ continue; } int j=i+1,k=num.length-1; while (j\u0026lt;k) { int valuei=num[i]; int valuej=num[j]; int valuek=num[k]; int sum = valuei+valuej+valuek; if(sum==0){ ans.add(new ArrayList\u0026lt;\u0026gt;(Arrays.asList(valuei,valuej,valuek))); } if(sum\u0026gt;0){ while (j\u0026lt;k\u0026amp;\u0026amp;num[k]==valuek) { k--; } }else{ while (j\u0026lt;k\u0026amp;\u0026amp;num[j]==valuej) { j++; } } } } return ans; } 小贴士 continue所在语句块的num[i]==num[i-1]主要是跳过重复的基准数，避免生成重复三元组。 num[j]==valuej和num[k]==valuek的作用也是一样的，本质上都是排序后数组的「重复元素相邻」，要跳过重复值。 第一个if语句只负责「添加结果」，和后续的逻辑无关，第二个独立的if-else负责「指针移动+去重」，覆盖所有情况。 ","date":"2026-01-17T21:16:58Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%89%E6%8C%87%E9%92%88-%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C/","title":"三指针-三数之和"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 public int minNumberDisappeared (int[] nums) { HashSet\u0026lt;Integer\u0026gt; set = new HashSet\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; nums.length; i++) { set.add(nums[i]); } int res = 1; while (set.contains(res)) { res++; } return res; } 小贴士 利用HashSet的O (1) 快速查找特性，先把数组所有元素存入哈希表 ，再从最小正整数1开始依次校验，找到第一个不在哈希表中的正整数，就是答案。\n","date":"2026-01-17T20:07:30Z","permalink":"https://iamxurulin.github.io/p/%E5%93%88%E5%B8%8C-%E7%BC%BA%E5%A4%B1%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AA%E6%AD%A3%E6%95%B4%E6%95%B0/","title":"哈希-缺失的第一个正整数"},{"content":"在RabbitMQ中，实现延迟消息可以通过死信交换机（DLX）+过期时间（TTL）的方案，也可以采用RabbitMQ官方提供的延迟消息插件。\nDLX+TTL。 主要步骤如下：\n1.声明并创建普通交换机和普通队列，完成二者的绑定。\n2.声明并创建死信交换机和死信队列，也完成二者的绑定。\n3.指定当前普通队列的死信要转发到哪个DLX，以及要匹配哪个死信路由键。\n4.不给普通队列配置任何的消费者，使普通队列只做消息暂存和过期，不消费。\n5.在生产者发送消息时，为消息设置TTL，将消息发送到普通队列和普通交换机。\n6当消息在普通队列的存活时间达到TTL后，过期的消息会被自动转发到DLX中，DLX再重新路由到死信队列，从死信队列中发送的消息就是延迟消息。\nrabbitmq_delayed_message_exchange插件 主要步骤：\n1.将插件放到RabbitMQ的plugins目录\n2.执行rabbitmq-plugins enable rabbitmq_delayed_message_exchange命令启用插件\n3.声明一个延迟交换机\n4.声明一个普通队列并绑定该延迟交换机\n5.生产者在发送消息时，设置消息投x-dlay:延迟时间(ms)\n","date":"2026-01-17T18:19:27Z","permalink":"https://iamxurulin.github.io/p/rabbitmq%E6%80%8E%E4%B9%88%E5%AE%9E%E7%8E%B0%E5%BB%B6%E8%BF%9F%E6%B6%88%E6%81%AF/","title":"RabbitMQ怎么实现延迟消息"},{"content":"JIT（Just In Time）即时编译，是一种在程序运行时将字节码转换为机器码的技术。\nJIT在Java程序运行的时候，如果发现了频繁执行的代码段，我们称之为热点代码，就会将这段热点代码编译成机器码，从而减少解释执行的开销。\nJIT有CLient Compiler和Server Compiler两种编译类型，\n其中，Client Compiler适用于客户端应用程序，主要用于快速启动的轻量级优化；\nServer Compiler适用于服务端应用程序，主要用于长时间运行的重度优化。\n","date":"2026-01-17T18:18:18Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4java%E4%B8%AD%E7%9A%84jit/","title":"说说Java中的JIT"},{"content":"Write Barrier可以理解为JVM在对象引用赋值操作中插入的一段AOP代码，通过这段代码，在应用程序运行期间，垃圾收集器可以感知到对象引用关系的变化，从而维护记忆集（RSet）、卡表（Card Table）等必要的数据结构，避免全堆扫描。\nWrite Barrier按时机分，有Pre-Write Barrier和Post-Write Barrier两种类型。\nPre-Write Barrier在对象引用改变前触发，用于记录引用变更前的状态；\nPost-Write Barrier和Pre-Write Barrier相反，是在对象引用改变后触发，用于记录引用变更后的状态。\nLogging是一种策略，在G1收集器中，Post和Pre Write Barrier都采用了Logging机制来实现，也就是先记录日志，后异步处理。\n那为什么要用Logging这样一种策略呢？\n主要是如果每次引用修改都直接去更新RSet这种复杂的全局数据结构，这样会严重拖慢应用线程，并可能会导致多线程下的伪共享问题。\n如果采用Logging机制，将对象的引用变更记录丢给应用线程负责，让后台线程负责复杂的处理，使对象的引用变更记录和处理解耦，从而保证在GC正确的前提下，最大程度地减少对应用程序吞吐量的影响。\n","date":"2026-01-17T18:17:05Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4java%E4%B8%AD%E7%9A%84logging-write-barrier/","title":"说说Java中的Logging-Write-Barrier"},{"content":"大模型微调（Fine-tuning）是指在预训练模型的基础上，使用特定任务的数据对模型进行再训练，以适应特定应用场景的需求。\n在实际的应用中，可以采取全模型微调、部分微调、参数高效微调等不同的策略，以适应不同的场景。\n常见的微调任务主要🈶️以下几类：\n1.将文本分为不同的类别\n2.识别文本中的人名、地名等实体\n3.根据指定的问题，从文本总寻找答案\n4.生成摘要、段落总结等于输入相关的文本\n5.将某种语言的文本翻译成另外一种语言\n6.处理图文匹配、生成图像描述等多种类型的数据\n","date":"2026-01-17T18:16:33Z","permalink":"https://iamxurulin.github.io/p/%E5%B8%B8%E8%A7%81%E7%9A%84%E5%BE%AE%E8%B0%83%E4%BB%BB%E5%8A%A1/","title":"常见的微调任务"},{"content":"MCP（Model Context Protocol）模型上下文协议，主要是为大语言模型和AI助手提供一个统一的标准化接口，突破了模型对静态知识库的依赖，使其具备了更强的动态交互能力。\n像数据库、API等其他服务，只要是通过MCP接入，大语言模型都能理解并利用这些资源，从而扩展其功能和应用范围。\nMCP主要由以下3个方面的作用：\n1.MCP提供了统一的协议接口，可以实现一次集成，随处连接，简化了模型与外部系统的集成过程。\n2.MCP能够使模型实时地访问最新的数据和工具。\n3.MCP使得系统的各个组件可以更加模块化地协作，从而降低了系统的维护成本和出错概率。\n","date":"2026-01-17T18:15:49Z","permalink":"https://iamxurulin.github.io/p/mcp%E5%8D%8F%E8%AE%AE%E5%9C%A8%E5%A4%A7%E6%A8%A1%E5%9E%8B%E4%B8%AD%E7%9A%84%E4%BD%9C%E7%94%A8/","title":"MCP协议在大模型中的作用"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public int[] twoSum (int[] numbers, int target) { HashMap\u0026lt;Integer,Integer\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); int[] ans = {-1,-1}; for(int i=0;i\u0026lt;numbers.length;i++){ if(map.containsKey(target-numbers[i])){ ans[0]= map.get(target-numbers[i])+1; ans[1]=i+1; break; }else{ map.put(numbers[i], i); } } return ans; } 小贴士 对每个数字 numbers[i]，找之前已经遍历过的数字中，有没有 target - numbers[i]。\n如果有，说明找到了答案。\n那答案的两个下标就分别是：\n之前的数的下标：map.get(target - numbers[i]) （map 里存的都是之前遍历过的数）\n当前数的下标：i ，题目要求返回 从 1 开始的下标，所以两个下标值都要 +1。\n再说说返回的下标必须按升序排列：\n由于HashMap 中存的元素的下标是「之前遍历的下标」，当前正在遍历的元素的下标是「现在的下标」，那必然满足「之前遍历的下标」 \u0026lt; 「现在的下标」，所以采用HashMap 解法能天然保证升序。\n","date":"2026-01-17T11:13:46Z","permalink":"https://iamxurulin.github.io/p/%E5%93%88%E5%B8%8C-%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C/","title":"哈希-两数之和"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 public int MoreThanHalfNum_Solution (int[] numbers) { HashMap\u0026lt;Integer,Integer\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); for(int i=0;i\u0026lt;numbers.length;i++){ map.put(numbers[i], map.getOrDefault(numbers[i],0)+1); if(map.get(numbers[i])\u0026gt;(numbers.length/2)){ return numbers[i]; } } return 0; } 小贴士 map.getOrDefault(k, defaultValue) ：获取 key也就是k对应的值，存在则返回原值，不存在则返回默认值defaultValue\n","date":"2026-01-17T11:13:35Z","permalink":"https://iamxurulin.github.io/p/%E5%93%88%E5%B8%8C-%E6%95%B0%E7%BB%84%E4%B8%AD%E5%87%BA%E7%8E%B0%E6%AC%A1%E6%95%B0%E8%B6%85%E8%BF%87%E4%B8%80%E5%8D%8A%E7%9A%84%E6%95%B0%E5%AD%97/","title":"哈希-数组中出现次数超过一半的数字"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public int[] FindNumsAppearOnce (int[] nums) { HashMap\u0026lt;Integer,Integer\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); int[] ans = new int[2]; for(int i=0;i\u0026lt;nums.length;i++){ map.put(nums[i],map.getOrDefault(nums[i],0)+1); } int j=0; for(int num:map.keySet()){ if(map.get(num)==1){ ans[j++]=num; } if(j==2){ break; } } if(ans[0]\u0026gt;ans[1]){ int t = ans[0]; ans[0]=ans[1]; ans[1]=t; } return ans; } 小贴士 for(int num : map.keySet())，我们很容易想到的是再一次遍历nums，但是这里可以利用HashMap的keySet，只遍历不重复的数字。\n","date":"2026-01-17T11:13:21Z","permalink":"https://iamxurulin.github.io/p/%E5%93%88%E5%B8%8C-%E6%95%B0%E7%BB%84%E4%B8%AD%E5%8F%AA%E5%87%BA%E7%8E%B0%E4%B8%80%E6%AC%A1%E7%9A%84%E4%B8%A4%E4%B8%AA%E6%95%B0%E5%AD%97/","title":"哈希-数组中只出现一次的两个数字"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 HashMap\u0026lt;Character,Integer\u0026gt; map = new HashMap\u0026lt;Character,Integer\u0026gt;(){{ put(\u0026#39;+\u0026#39;,1); put(\u0026#39;-\u0026#39;,1); put(\u0026#39;*\u0026#39;,2); }}; void calc(LinkedList\u0026lt;Integer\u0026gt; nums,LinkedList\u0026lt;Character\u0026gt; operators){ if(nums.isEmpty()||nums.size()\u0026lt;2){ return; } if(operators.isEmpty()){ return; } int b = nums.pollLast(); int a = nums.pollLast(); char operator = operators.pollLast(); int res = 0; if(operator==\u0026#39;+\u0026#39;){ res = a+b; }else if(operator==\u0026#39;-\u0026#39;){ res = a-b; }else if(operator==\u0026#39;*\u0026#39;){ res = a*b; } nums.addLast(res); } public int solve(String s) { s = s.replaceAll(\u0026#34; \u0026#34;, \u0026#34;\u0026#34;); char[] str = s.toCharArray(); int n = s.length(); LinkedList\u0026lt;Integer\u0026gt; nums = new LinkedList\u0026lt;\u0026gt;(); LinkedList\u0026lt;Character\u0026gt; operators = new LinkedList\u0026lt;\u0026gt;(); nums.addLast(0); for(int i=0;i\u0026lt;n;i++){ char c = str[i]; if(c==\u0026#39;(\u0026#39;){ operators.addLast(c); }else if(c==\u0026#39;)\u0026#39;){ while(!operators.isEmpty()){ if(operators.peekLast()!=\u0026#39;(\u0026#39;){ calc(nums,operators); }else{ operators.pollLast(); break; } } }else{ if(Character.isDigit(c)){ int base = 0; int j=i; while(j\u0026lt;n\u0026amp;\u0026amp;Character.isDigit(str[j])){ base=base*10+(str[j++]-\u0026#39;0\u0026#39;); } nums.addLast(base); i=j-1; }else{ if(i\u0026gt;0\u0026amp;\u0026amp;(str[i-1]==\u0026#39;(\u0026#39;||str[i-1]==\u0026#39;+\u0026#39;||str[i-1]==\u0026#39;-\u0026#39;)){ nums.addLast(0); } while(!operators.isEmpty()\u0026amp;\u0026amp;operators.peekLast()!=\u0026#39;(\u0026#39;){ char prev = operators.peekLast(); if(map.get(prev)\u0026gt;=map.get(c)){ calc(nums,operators); }else{ break; } } operators.addLast(c); } } } while(!operators.isEmpty()\u0026amp;\u0026amp;operators.peekLast()!=\u0026#39;(\u0026#39;){ calc(nums,operators); } return nums.peekLast(); } 小贴士 1.nums.addLast(0) 主要用于处理【表达式以正负号开头】的场景，比如 -1+2、+3*4，可以把 -1 转为 0-1，把 +3 转为 0+3\n2.i = j - 1可以修正外层 for 循环的索引i，让i直接跳到「当前多位数的最后一位」，抵消 for 循环的自动i++，避免重复遍历已经处理过的数字字符。、\n比如表达式123+4，j=3：\n执行 i = j - 1 → i = 3 - 1 = 2，然后，外层 for 循环执行自动 i++ → i从2变成3，下一轮循环，i=3，正好读取字符str[3] → '+'。\n","date":"2026-01-16T22:24:33Z","permalink":"https://iamxurulin.github.io/p/%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B1%82%E5%80%BC/","title":"表达式求值"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 private PriorityQueue\u0026lt;Integer\u0026gt; maxHeap = new PriorityQueue\u0026lt;\u0026gt;((a, b)-\u0026gt;(b - a)); //大顶堆存较小的一半 private PriorityQueue\u0026lt;Integer\u0026gt; minHeap = new PriorityQueue\u0026lt;\u0026gt;((a, b)-\u0026gt;(a - b)); public void Insert(Integer num) { if (maxHeap.isEmpty() || num \u0026lt;= maxHeap.peek()) { maxHeap.add(num); } else { minHeap.add(num); } balance(); } public Double GetMedian() { if (maxHeap.size() == minHeap.size()) { return (double)((maxHeap.peek() + minHeap.peek()) / 2.0); } else { return (double)(maxHeap.size() \u0026gt; minHeap.size() ? maxHeap.peek() : minHeap.peek()); } } private void balance() { if (Math.abs(maxHeap.size() - minHeap.size()) == 2) { if (maxHeap.size() \u0026gt; minHeap.size()) { minHeap.add(maxHeap.poll()); } else { maxHeap.add(minHeap.poll()); } } } 小贴士 1.Java的PriorityQueue默认是小顶堆，这里采用大顶堆maxHeap存储较小的一半，用小顶堆存储较大的一半。\n2.(maxHeap.peek() + minHeap.peek()) / 2.0，除以2.0 触发浮点运算，不会丢失小数位\n","date":"2026-01-16T20:41:32Z","permalink":"https://iamxurulin.github.io/p/%E5%A4%A7%E9%A1%B6%E5%A0%86-%E5%B0%8F%E9%A1%B6%E5%A0%86-%E6%95%B0%E6%8D%AE%E6%B5%81%E4%B8%AD%E7%9A%84%E4%B8%AD%E4%BD%8D%E6%95%B0/","title":"大顶堆+小顶堆-数据流中的中位数"},{"content":"今天在本地调好代码之后，准备把代码提交到仓库中，在命令行出现了以下报错：\n1 2 3 4 5 6 7 8 To github.com:iamxurulin/static-resource.git ! [rejected] main -\u0026gt; main (fetch first) error: failed to push some refs to \u0026#39;github.com:iamxurulin/static-resource.git\u0026#39; hint: Updates were rejected because the remote contains work that you do not hint: have locally. This is usually caused by another repository pushing to hint: the same ref. If you want to integrate the remote changes, use hint: \u0026#39;git pull\u0026#39; before pushing again. hint: See the \u0026#39;Note about fast-forwards\u0026#39; in \u0026#39;git push --help\u0026#39; for details. 我开始以为是GitHub（远程仓库）上有一些本地仓库里没有的更新，所以执行了强制推送的指令：\n1 git push -f origin main 执行完操作后，我就跑去源代码仓库查看，发现没有提交记录。\n后面跑去static-resource仓库一看才发现，刚才把代码全部提交到这个仓库了，并且git 的推送机制把本地历史记录强制同步到了远程，把这个仓库原有的commit记录全部覆盖了，想通过SHA哈希值恢复到以前的版本也恢复不了。\n怎么办呢？\nGitHub 个人动态在执行强制推送时，会记录这一动作，但是我打开 GitHub 个人主页在页面中间的 Contribution activity（贡献动态）中也没有找到最近几小时的commit记录。\n别急，Git 是有“后悔药”的。\n既然有两个仓库，本地电脑上是不是应该有两个文件夹？\n我进入到存放 static-resource 的文件夹，右键 -\u0026gt; Git Bash Here ，输入 git log -1。\n这时，屏幕上显示了 commit xxxxxxxx 。\n接下来，我在这个文件夹打开终端，输入：\n1 git push -f https://github.com/username/static-resource.git xxxxxxxx:main 之后，打开static-resource仓库，发现已经恢复了。\n所以，后续推送的时候记得执行：\n1 git remote -v 确认一下要推送到的远程仓库。\n","date":"2026-01-16T17:59:10Z","permalink":"https://iamxurulin.github.io/p/%E6%8E%A8%E9%80%81%E9%94%99%E4%BA%86%E4%BB%93%E5%BA%93%E7%9A%84%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95/","title":"推送错了仓库的解决办法"},{"content":"1.初始标记（initial mark）\nCMS会标记所有根对象直接可达的对象。\n2.并发标记（Concurrent marking）\n并发标记阶段，垃圾收集器和应用程序并发执行，从根对象直接可达的对象开始进行tracing，递归的扫描所有可达对象。\n3.并发预清理（Concurrent precleaning）\n并发预清理阶段会先分担一些重新标记阶段的工作，扫描卡表脏的区域以及新晋升到老年代的对象。\n4.可中断的预清理阶段（AbortablePreclean）\n可中断的预清理阶段也是先分担一些重新标记阶段的工作，和并发预清理阶段不同的是这个阶段可以中断。\n5.重新标记（remark）\n因为前几个并发阶段会导致引用关系发生变化，所有需要重新遍历一遍新生代对象、GC Roots和卡表等，从而对标记进行修正，需要花较长的时间来进行重新扫描。\n6.并发清理（Concurrent Sweeping）\n并发清理阶段，标记为不可达的对象会被清理。\n7.并发重置（Concurrent Reset）\n重置CMS内部的状态。\n","date":"2026-01-16T17:20:13Z","permalink":"https://iamxurulin.github.io/p/java%E7%9A%84cms%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%B5%81%E7%A8%8B/","title":"Java的CMS垃圾回收流程"},{"content":"Linux系统中的日志文件通常都存储在/var/log目录下。\nmessages 记录大多数系统范围内的普通信息和错误消息 syslog 系统日志文件，记录系统范围内包括启动、内核事件设备信息等消息 auth.log 记录授权相关的日志，比如用户的登录与登出 kern.log 记录内核产生的日志信息 dmesg 记录设备和驱动程序的初始化信息，以及内核启动期间产生的消息 boot.log 记录系统启动过程中发生的事件 faillog 记录失败的登录尝试信息 ","date":"2026-01-16T17:19:26Z","permalink":"https://iamxurulin.github.io/p/linux%E7%B3%BB%E7%BB%9F%E4%B8%AD%E5%B8%B8%E7%94%A8%E7%9A%84%E6%97%A5%E5%BF%97%E6%96%87%E4%BB%B6/","title":"Linux系统中常用的日志文件"},{"content":"在Java中，允许子类复用父类的字段和方法，用extends关键字建立继承关系。\nJava只支持单继承，也就是一个类最多只能有一个直接父类。\n子类可以继承父类的public和protected成员；对于父类的private成员，子类只能通过父类提供的public/protected方法来间接地访问。\n子类的构造方法执行之前必须先调用父类的构造方法。\n如果父类存在无参数的构造方法，则编译器会自动加上super();\n如果父类不存在无参数的构造方法，只存在带参数的构造方法，则子类必须显示地调用super(参数)，因为编译器默认插入的是无参的super()方法，会导致编译报错。\n继承在带来代码复用的同时也带来了紧耦合问题。\n子类在重写父类的方法时，只能放大或者保持方法的访问权限，不能缩小访问权限。\n也就是说，\n父类如果是public方法，则子类只能是public；\n父类如果是protected方法，则子类可以是protected或者public。\n因为在父类引用指向子类对象时，调用方是期望能访问到这个方法的，如果子类缩小了这个权限，就会导致调用方无法访问。\n","date":"2026-01-16T16:29:52Z","permalink":"https://iamxurulin.github.io/p/java%E4%B8%AD%E7%9A%84%E7%BB%A7%E6%89%BF%E6%9C%BA%E5%88%B6/","title":"Java中的继承机制"},{"content":"包含MCP主机、MCP客户端、MCP服务器、本地数据源和远程服务五大核心组件。\nMCP 主机是希望通过MCP访问数据的程序。\nMCP Server主要负责向 LLM 提供结构化上下文和可调用的操作能力。\nMCP Server定义了资源、工具、提示三大类基础功能，这三类能力为语言模型提供了更强的上下文输入和交互能力。\nMCP服务器可以安全地访问用户的计算机文件、数据库和服务这些本地数据源。\nMCP服务器可以通过API连接到外部的系统。\nMCP客户端是连接大语言模型与MCP服务器的桥梁，负责在模型与外部工具之间传递信息和协调操作。\n","date":"2026-01-16T16:28:50Z","permalink":"https://iamxurulin.github.io/p/mcp%E6%9E%B6%E6%9E%84%E5%8C%85%E5%90%AB%E5%93%AA%E4%BA%9B%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6/","title":"MCP架构包含哪些核心组件"},{"content":"主流的向量数据库有Milvus、Pinecone、Weaviate、Qdrant、Chroma等。\n1. Milvus 支持TB级向量的增删改操作，以及近实时查询；\n适合推荐系统、图像检索、NLP等场景。\n2.Pinecone 开箱即用，高并发性能好；\n适合实时搜索、快速原型开发等场景；\n缺点是按使用量计费，使用成本较高；\n3.Weaviate 支持文本和图像等多模态数据；\n适合知识图谱和智能问答场景；\n缺点是复杂查询时的延迟较高；\n4.Qdrant 支持向量与元数据联合搜索；\n适合推荐系统中元数据与向量结合的复杂查询；\n5.Chroma 专注嵌入式向量存储，支持本地化部署；\n适合中小规模数据；\n","date":"2026-01-16T16:27:12Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4%E4%BD%A0%E4%BA%86%E8%A7%A3%E7%9A%84%E5%90%91%E9%87%8F%E6%95%B0%E6%8D%AE%E5%BA%93/","title":"说说你了解的向量数据库"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 private void swap(int[] arr,int i,int j){ if(i!=j){ arr[i]^=arr[j]; arr[j]^=arr[i]; arr[i]^=arr[j]; } } public int findKth (int[] a, int n, int K) { K=n-K; int low = 0,high = n-1; while(low\u0026lt;=high){ int i = low; for(int j=low+1;j\u0026lt;=high;j++){ if(a[j]\u0026lt;a[low]){ swap(a,j,++i); } } swap(a, low, i); if(K==i){ return a[i]; }else if(K\u0026lt;i){ high=i-1; }else{ low=i+1; } } return -1; } 小贴士 数组中第K大的数对应的就是数组下标为n-K的数\n","date":"2026-01-16T07:59:31Z","permalink":"https://iamxurulin.github.io/p/%E5%BF%AB%E6%8E%92%E6%80%9D%E6%83%B3-%E5%AF%BB%E6%89%BE%E7%AC%ACk%E5%A4%A7/","title":"快排思想-寻找第K大"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public ArrayList\u0026lt;Integer\u0026gt; GetLeastNumbers_Solution (int[] input, int k) { ArrayList\u0026lt;Integer\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); PriorityQueue\u0026lt;Integer\u0026gt; pq = new PriorityQueue\u0026lt;\u0026gt;((a, b)-\u0026gt;(b - a)); if (input == null || k \u0026lt;= 0 || input.length == 0) { return ans; } for (int val : input) { if (pq.size() \u0026lt; k) { pq.add(val); } else { if (pq.peek() \u0026gt; val) { pq.poll(); pq.add(val); } } } while (!pq.isEmpty()) { ans.add(pq.poll()); } return ans; } 小贴士 这里解释一下为什么是 pq.size() \u0026lt; k？\npq.size() \u0026lt; k 意味着只要堆里的元素还没凑够 k 个，就无脑往堆里加元素，不用任何筛选；\n当 pq.size() == k 时，堆就「满了」，此时会进入 else 分支，也就是【筛选替换阶段】，堆顶就是「当前筛选出的最小 k 个数的最大值」，这个值就是筛选门槛， 所有后续遍历到的数字，都只用和这个「门槛」比较就行。\n如果写成了pq.size() \u0026lt;= k，就变成了只要堆里的元素 ≤k 个，就继续往堆里加元素到size=k的时候，size\u0026lt;=k是 true，但是执行 add 之后，下一次循环的判断条件还是pq.size()\u0026lt;=k，此时 size=k，依然是 true，但堆的 size 不会再涨了，堆永远不会进入 else 筛选替换分支，从头到尾只会无脑入堆，else分支里的逻辑完全失效。\n","date":"2026-01-16T07:58:54Z","permalink":"https://iamxurulin.github.io/p/%E4%BC%98%E5%85%88%E9%98%9F%E5%88%97-%E6%9C%80%E5%B0%8F%E7%9A%84k%E4%B8%AA%E6%95%B0/","title":"优先队列-最小的K个数"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class MonotonicQueue { LinkedList\u0026lt;Integer\u0026gt; queue = new LinkedList\u0026lt;\u0026gt;(); public void push(int n){ while(!queue.isEmpty()\u0026amp;\u0026amp;queue.getLast()\u0026lt;n){ queue.pollLast(); } queue.addLast(n); } public void pop(int n){ if(n==queue.getFirst()){ queue.pollFirst(); } } public int max(){ return queue.getFirst(); } } public ArrayList\u0026lt;Integer\u0026gt; maxInWindows (int[] num, int size) { MonotonicQueue window = new MonotonicQueue(); ArrayList\u0026lt;Integer\u0026gt; res = new ArrayList\u0026lt;\u0026gt;(); if(num==null||size\u0026lt;=0||num.length==0||size\u0026gt;num.length){ return res; } for(int i=0;i\u0026lt;num.length;i++){ if(i\u0026lt;size-1){ window.push(num[i]); }else{ window.push(num[i]); res.add(window.max()); window.pop(num[i-size+1]); } } return res; } 小贴士 1.构建一个特殊的【单调队列】来充当不断滑动的窗口：\n这个单调队列的队首记录了滑动过程中的最大值（对应max方法），从队首到队尾的元素值大小是单调递减的；\n当队列为空或者尾部的元素（getLast）小于要入队的元素时，将该尾部元素弹出（pollLast）,元素是从队列尾部入队的（addLast）。\n当滑动窗口需要移除的元素等于队列头部（当前最大值）时，将队首元素弹出（pollFirst）；\n2.解释一下这个i-size+1\n对于长度固定为size的滑动窗口，当窗口还没填满时，不考虑计算最大值和移除元素，直到把前size-1个元素填满；\n从第size个元素开始（对应就是下标size-1），窗口正式填满，此时，依次完成把当前元素加入窗口，将窗口的最大值记录到res中，再把窗口最左侧的元素移除，为下一步滑动腾出一个位置。\n那窗口最左侧元素的下标是什么呢？\n因为当前元素是num[i]，所以此时的窗口的右边界是i，又因为窗口的大小固定为size，所以，窗口的左边界就是i-size+1，这也就是我们要移除的窗口最左侧元素的下标。\n","date":"2026-01-15T21:58:40Z","permalink":"https://iamxurulin.github.io/p/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97-%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%9A%84%E6%9C%80%E5%A4%A7%E5%80%BC/","title":"单调队列-滑动窗口的最大值"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public boolean isValid (String s) { char[] str = s.toCharArray(); Stack\u0026lt;Character\u0026gt; stackData = new Stack\u0026lt;\u0026gt;(); for(char c:str){ if(c==\u0026#39;(\u0026#39;){ stackData.push(\u0026#39;)\u0026#39;); }else if(c==\u0026#39;[\u0026#39;){ stackData.push(\u0026#39;]\u0026#39;); }else if(c==\u0026#39;{\u0026#39;){ stackData.push(\u0026#39;}\u0026#39;); }else if(stackData.isEmpty()||c!=stackData.pop()){ return false; } } return stackData.isEmpty(); } 小贴士 如果遇到了左括号，就把和这个左括号配对的右括号压入栈中，总共就3种括号：小括号、中括号和花括号；\n如果遇到了右括号，分情况讨论：\n如果此时栈为空，说明无法构成有效的括号；\n如果此时栈不为空，并且出栈的元素不等于这个右括号；\n这两种情况，都构不成有效的括号，返回false；\n只有到最后栈为空了，才返回true。\n","date":"2026-01-15T20:28:38Z","permalink":"https://iamxurulin.github.io/p/%E6%9C%89%E6%95%88%E6%8B%AC%E5%8F%B7%E5%BA%8F%E5%88%97/","title":"有效括号序列"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 Stack\u0026lt;Integer\u0026gt; stackData1 = new Stack\u0026lt;Integer\u0026gt;(); Stack\u0026lt;Integer\u0026gt; stackData2 = new Stack\u0026lt;Integer\u0026gt;(); public void push(int node) { stackData1.push(node); if(stackData2.isEmpty()||node\u0026lt;=min()){ stackData2.push(node); } } public void pop() { if(stackData1.peek().equals(min())){ stackData2.pop(); } stackData1.pop(); } public int top() { return stackData1.peek(); } public int min() { return stackData2.peek(); } 小贴士 用 stackData1.peek().equals(min()) 来比较包装类的实际数值，也可以用 stackData1.peek().intValue() == min()，效果是一样的。\n","date":"2026-01-15T20:04:19Z","permalink":"https://iamxurulin.github.io/p/%E5%8C%85%E5%90%ABmin%E5%87%BD%E6%95%B0%E7%9A%84%E6%A0%88/","title":"包含min函数的栈"},{"content":"HNSW、LSH和PQ是向量数据库中的3种核心索引与压缩技术，用于加速高维向量的相似性搜索。\nHNSW Hierarchical Navigable Small World（HNSW），在高维空间中，构建多层图结构，每一层都是一个小世界网络。\n上层的节点比较稀疏，能快速跳跃式定位大致的范围；\n下层的节点比较密集，用于精细搜索。\nHNSW技术查询速度和精度的平衡比较优秀。\nLSH Locality-Sensitive Hashing（LSH），是由经过特殊设计的哈希函数，能够使相似向量以较高的概率映射到同一个哈希桶，不相似的向量尽量分散到不同的哈希桶。\n在查询的时候，只需要搜索查询向量所在的哈希桶以及相邻的哈希桶，极大地缩小了检索范围。\nLSH技术在推荐系统、图像检索等海量数据的近似查询场景中应用广泛。\nPQ Product Quantization（PQ）将高维的向量拆分成多个低维的子向量，对每个子向量集合进行聚类，生成聚类中心。\n在存储的时候，用聚类中心的编号表示向量，从而大幅减少存储空间。\nPQ技术常用于工业级的向量检索系统。\n","date":"2026-01-15T17:53:06Z","permalink":"https://iamxurulin.github.io/p/%E8%A7%A3%E9%87%8A%E4%B8%80%E4%B8%8B%E5%90%91%E9%87%8F%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%AD%E7%9A%84hnswlsh%E5%92%8Cpq/","title":"解释一下向量数据库中的HNSW、LSH和PQ"},{"content":"Copilot模式（副驾驶协助模式）是大模型作为“助手”提供实时的建议，比如代码补全、文案的润色等等，最终的决策权保留在用户手中\nAgent模式（代理智能体模式）是可以自主驱动，大模型可以独立的拆解任务、调用工具完成端到端的操作。\n除了这两个模式之外，还有一个Embedding模式（嵌入模式），这个模式主要是在后台进行辅助，将大模型作为一个隐藏的组件集成到现有的系统中，这种模式用户是无法感知的。\nEmbedding模式有很多的实际应用的例子，比如在推荐系统中，电商平台用Embedding理解商品和用户的兴趣，但是用户往往只能看到“猜你喜欢”。\n","date":"2026-01-15T17:32:27Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4copilot%E6%A8%A1%E5%BC%8F%E5%92%8Cagent%E6%A8%A1%E5%BC%8F%E7%9A%84%E5%8C%BA%E5%88%AB/","title":"说说Copilot模式和Agent模式的区别"},{"content":"RabbitMQ的集群主要分为标准集群、镜像集群和联邦集群三种。\n标准集群 标准集群是多台RabbitMQ服务器通过网络连接组成一个集群，所有节点共享元数据，但是一个消息只存储在单个节点上。\n在标准集群模式下，因为元数据是共享的，所以任何一个节点都知道消息在哪个节点上，可以实现负载均衡。\n消息存储在某个节点上但是不会自动复制到其他节点。\n镜像集群 和标准集群模式不同，镜像集群模式下，队列的消息会复制到多个节点上，如果主队列所在的节点发生故障，副本节点会自动接管，从而保证队列的可用性。\nFederated 集群 通过Federated Exchange或Federated Queue实现消息的跨节点路由 / 复制，节点之间无需共享元数据，解决了标准 / 镜像集群无法跨广域网部署的问题。\n","date":"2026-01-15T17:21:36Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4rabbitmq%E7%9A%84%E9%9B%86%E7%BE%A4%E6%A8%A1%E5%BC%8F/","title":"说说RabbitMQ的集群模式"},{"content":"Computer Use是让Claude能够操作计算机。\n实际工作原理：\nClaude通过截图观察屏幕的内容，基于视觉理解，Claude计算出需要点击或者输入的像素坐标，通过API发送鼠标移动、点击和键盘输入等指令，执行操作后再次截图，观察结果并决定下一步。\nComputer Use场景：\n在网页表单中填写信息并提交\n执行需要多个软件协同的复杂工作流\n操作如PS、PPT、Excel等桌面应用程序\n","date":"2026-01-15T16:54:17Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4computer-use%E7%9A%84%E5%8E%9F%E7%90%86/","title":"说说Computer-Use的原理"},{"content":"Manus是由Monica.im团队于2025年3月推出的全球首款通用型AI智能体，它的重大突破在于能够独立思考、规划并执行复杂的任务，可以直接交付诸如股票分析结果、简历筛选结果这些完整的成果。\nManus主要是通过一个中央模块，将用户的指令拆解为多个子任务，再通过不同的内部智能体或者工具执行，形成端到端的自动化执行流程。\nManus的底层还是调用了诸如Claude、ChatGPT这些大模型来实现规划和决策。\nManus的执行过程可以分为4个主要阶段：\n1.规划器 基于LLM生成总体执行计划，并拆分子任务。\n2.智能体/工具 每个子任务可以由不同的模型或者外部的API完成。\n3.状态驱动和条件分支 执行过程中根据中间结果动态地决定是否需要重试、是否需要切换方案或者是否需要提前结束。\n4.可回溯和异步执行 可记录全过程的执行“Trace”，可在Web控制台进行Debug，也支持后台的长期运行。\n","date":"2026-01-15T15:50:33Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4%E4%BD%A0%E5%AF%B9manus%E7%9A%84%E4%BA%86%E8%A7%A3/","title":"说说你对Manus的了解"},{"content":"方法1 打开 GitHub，找到要下载的「目标文件夹」，点击进入这个文件夹的页面 复制浏览器地址栏里的「文件夹完整 URL」（比如：https://github.com/letere-gzj/live2d-widget-v3/tree/main/Resources） 打开 DownGit 官网： https://minhaskamal.github.io/DownGit/ 把第2步复制的文件夹 URL 粘贴到 DownGit 的输入框里，点击「Download」按钮即可 方法2 可以选择在【桌面】打开命令行窗口：\n初始化克隆仓库 1 git clone --filter=blob:none --no-checkout https://github.com/用户名/仓库名.git 进入克隆的仓库目录 1 cd 仓库名 开启「稀疏检出」功能，告诉Git：我只需要指定文件夹 1 git sparse-checkout init --cone 指定要下载的「单个文件夹路径」（路径从仓库根目录开始写，不用加/） 1 git sparse-checkout set 文件夹路径/文件夹名称 拉取指定文件夹的所有内容 如果是仓库的其他分支名，比如master分支，需要将main换成master\n1 git checkout main 以下载https://github.com/letere-gzj/live2d-widget-v3/tree/main/Resources这个文件夹举例，依次输入以下指令：\n1 2 3 4 5 6 7 8 9 git clone --filter=blob:none --no-checkout https://github.com/letere-gzj/live2d-widget-v3.git cd live2d-widget-v3 git sparse-checkout init --cone git sparse-checkout set Resources git checkout main ","date":"2026-01-15T12:25:31Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%8B%E8%BD%BD-github-%E4%BB%93%E5%BA%93%E5%8D%95%E4%B8%AA%E6%96%87%E4%BB%B6%E5%A4%B9%E7%9A%84%E6%96%B9%E6%B3%95/","title":"下载-GitHub-仓库「单个文件夹」的方法"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Stack\u0026lt;Integer\u0026gt; stack1 = new Stack\u0026lt;Integer\u0026gt;(); Stack\u0026lt;Integer\u0026gt; stack2 = new Stack\u0026lt;Integer\u0026gt;(); public void push(int node) { stack1.push(node); } public int pop() { if(stack2.isEmpty()){ while (!stack1.isEmpty()) { stack2.push(stack1.pop()); } } return stack2.pop(); } 小贴士 所有入队的元素，无条件直接压入 stack1 stack1 只管入队，stack2 只管出队，并且只有当stack2 为空时才进行一次性倒栈 ","date":"2026-01-15T00:14:18Z","permalink":"https://iamxurulin.github.io/p/%E7%94%A8%E4%B8%A4%E4%B8%AA%E6%A0%88%E5%AE%9E%E7%8E%B0%E9%98%9F%E5%88%97/","title":"用两个栈实现队列"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 // 存储最终结果：key = 二叉树的层级（根节点在第0层），value = 该层级【最右侧】的节点值 private HashMap\u0026lt;Integer, Integer\u0026gt; ans = new HashMap\u0026lt;\u0026gt;(); // 预处理哈希表：key=中序数组的节点值，value=该值在中序数组的下标 private HashMap\u0026lt;Integer, Integer\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); // 前序数组的全局遍历指针：按「根→左→右」的前序规则，依次取当前递归层的根节点，取完自增 private int level = 0; public void buildTree(int[] preOrder, int[] inOrder, int left, int right, int i) { if (left \u0026gt; right) { return; } // 按前序顺序取根节点，通过哈希表拿到根节点在中序数组的下标index int index = map.get(preOrder[level++]); // 递归构建左子树，中序区间[left, index-1]，子节点层级 = 父层级+1 buildTree(preOrder, inOrder, left, index - 1, i + 1); // 递归构建右子树，中序区间[index+1, right]，子节点层级 = 父层级+1 buildTree(preOrder, inOrder, index + 1, right, i + 1); // 存入当前节点到对应层级，后存的节点会覆盖先存的 → 最终保留该层最右侧节点 ans.put(i, inOrder[index]); } public int[] solve (int[] preOrder, int[] inOrder) { for(int i=0;i\u0026lt;inOrder.length;i++){ map.put(inOrder[i], i); } buildTree(preOrder,inOrder,0,preOrder.length-1,0); int[] temp = new int[ans.size()]; for(int i=0;i\u0026lt;ans.size();i++){ temp[i]=ans.get(i); } return temp; } 小贴士 解释一下这行代码ans.put(i, inOrder[index]);可以拿到「每层最右侧」的节点：\n因为在递归时，是先递归构建左子树，再递归构建右子树，这个顺序意味着：在同一层级中，「左侧节点」会被先处理，「右侧节点」被后处理。\n对于哈希表来说，相同key的put操作，会覆盖原值，在同一层级中，不管有多少个节点，最终只会存最后一次 put 的那个值。\n所以，同一层级的右侧节点一定是最后一个被 put 的，所以最终ans.get(i)拿到的，一定是「第 i 层最右侧的节点值」。\n","date":"2026-01-14T23:55:54Z","permalink":"https://iamxurulin.github.io/p/%E6%96%B0%E8%A7%86%E8%A7%92-%E8%BE%93%E5%87%BA%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%8F%B3%E8%A7%86%E5%9B%BE/","title":"新视角-输出二叉树的右视图"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public TreeNode reConstructBinaryTree (int[] preOrder, int[] vinOrder) { int pre_len = preOrder.length; int vin_len = vinOrder.length; if (pre_len == 0 || vin_len == 0) { return null; } TreeNode root = new TreeNode(preOrder[0]); for (int i = 0; i \u0026lt; vinOrder.length; i++) { if (preOrder[0] == vinOrder[i]) { // 左子树：前序从[1, i+1) 中序从[0, i) root.left = reConstructBinaryTree(Arrays.copyOfRange(preOrder, 1, i + 1), Arrays.copyOfRange(vinOrder, 0, i)); // 右子树：前序从[i+1, pre_len) 中序从[i+1, vin_len) root.right = reConstructBinaryTree(Arrays.copyOfRange(preOrder, i + 1, preOrder.length), Arrays.copyOfRange(vinOrder, i + 1, vinOrder.length)); break; } } return root; } 小贴士 1 Arrays.copyOfRange(原数组, from, to) → 复制数组的[from, to)区间，返回新数组； 中序遍历分割数组比较好理解：\n中序遍历过程中左子树是从[0,i)，右子树是从[i+1,vin_len)\n前序遍历分割数组可能会有点绕，这里解释一下：\n当中序遍历到位置i时，可以得知左子树所在的区间是[0,i-1]，长度就是i；\n那么回到前序遍历中来，因为同一棵树它的左子树的长度在前序遍历和中序遍历的过程中是相同的，也就是长度是i，那么又因为前序遍历的0位置是根节点，则前序遍历的左子树所在的区间就是[1,i]。\n由于Arrays.copyOfRange方法是左闭右开区间，所以前序遍历过程中，左子树是从[1,i+1)，右子树就是从[i+1,pre_len)。\n","date":"2026-01-14T22:48:04Z","permalink":"https://iamxurulin.github.io/p/%E5%89%8D%E5%BA%8F-%E4%B8%AD%E5%BA%8F-%E9%87%8D%E5%BB%BA%E4%BA%8C%E5%8F%89%E6%A0%91/","title":"前序+中序-重建二叉树"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 String Serialize(TreeNode root) { StringBuilder sb = new StringBuilder(); if (root != null) { Queue\u0026lt;TreeNode\u0026gt; queue = new LinkedList\u0026lt;\u0026gt;(); queue.add(root); sb.append(root.val + \u0026#34;,\u0026#34;); // 先存入根节点值 while (!queue.isEmpty()) { root = queue.poll(); // 取出队首的待处理节点 // 严格先处理左孩子：有值存值+逗号，无值存# +逗号 if (root.left != null) { sb.append(root.left.val + \u0026#34;,\u0026#34;); queue.add(root.left); } else { sb.append(\u0026#34;#,\u0026#34;); } // 严格后处理右孩子：有值存值+逗号，无值存# +逗号 if (root.right != null) { sb.append(root.right.val + \u0026#34;,\u0026#34;); queue.add(root.right); } else { sb.append(\u0026#34;#,\u0026#34;); } } } return sb.toString(); } TreeNode Deserialize(String str) { if (str.equals(\u0026#34;\u0026#34;)) { return null; } String[] nodes = str.split(\u0026#34;,\u0026#34;); // 按分隔符拆分出所有节点内容 int index = 0; TreeNode root = generate(nodes[index++]); // 第一个元素是根节点 Queue\u0026lt;TreeNode\u0026gt; queue = new LinkedList\u0026lt;\u0026gt;(); queue.add(root); while (!queue.isEmpty()) { TreeNode cur = queue.poll(); // 取出待分配子节点的父节点 cur.left = generate(nodes[index++]); // 严格先分配左孩子 cur.right = generate(nodes[index++]); // 严格后分配右孩子 // 只有非空节点才有子节点，需要入队等待分配子节点 if (cur.left != null) queue.add(cur.left); if (cur.right != null) queue.add(cur.right); } return root; } TreeNode generate(String val) { return val.equals(\u0026#34;#\u0026#34;) ? null : new TreeNode(Integer.parseInt(val)); } 小贴士：\nInteger.parseInt () 和 Integer.valueOf () 这俩都是 Java 中把「数字格式的字符串」转为整数 的核心静态方法，最核心的区别只有一个： ✅ Integer.parseInt(String s) → 返回 基本数据类型 int ✅ Integer.valueOf(String s) → 返回 包装类对象 Integer\n","date":"2026-01-14T21:16:56Z","permalink":"https://iamxurulin.github.io/p/%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86-%E5%BA%8F%E5%88%97%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91/","title":"层序遍历-序列化二叉树"},{"content":" 求解代码 前文【非递归】二叉搜索树的最近公共祖先我们利用非递归+迭代的方式求出了二叉搜索树的最近公共祖先，主要还是利用了二叉搜索树左子树所有节点值\u0026lt;根节点值\u0026lt;右子树所有节点值的特性。\n但是，普通二叉树的节点值没有这样的规律，没办法通过数值的大小进行位置的判断力。\n本文使用递归的方式求普通二叉树的最近公共祖先：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public int lowestCommonAncestor (TreeNode root, int o1, int o2) { if (root == null) { return -1; } if (root.val == o1 || root.val == o2) { return root.val; } int left = lowestCommonAncestor(root.left, o1, o2); int right = lowestCommonAncestor(root.right, o1, o2); if (left == -1) { return right; } if (right == -1) { return left; } return root.val; } ","date":"2026-01-14T19:01:55Z","permalink":"https://iamxurulin.github.io/p/%E9%80%92%E5%BD%92-%E5%9C%A8%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E6%89%BE%E5%88%B0%E4%B8%A4%E4%B8%AA%E8%8A%82%E7%82%B9%E7%9A%84%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88/","title":"递归-在二叉树中找到两个节点的最近公共祖先"},{"content":"1.首先需要确保远程服务器和本地主机都安装了OpenSSH软件；\n2.如果采用用户名为“Javaer_12”连接到远程主机\u0026quot;192.168.1.114\u0026quot;，可以输入以下指令：\n1 ssh Javaer_12@192.168.1.114 3.第一次连接的时候可能会提示你确认主机的真实性，输入yes即可，再输入远程服务区的密码即可登录。\n⚠️ 注意：只能输完整的 yes，输 y 无效\n使用输入密码的方式进行登录，时间长了会感觉比较麻烦，这时可以考虑通过密钥验证的方式进行登录。\n通过密钥进行登录的大概就两步：\n先在本地生成ssh密钥对：\n1 ssh-keygen -t rsa 再将公钥复制到远程服务器上：\n1 ssh-copy-id Javaer_12@192.168.1.114 这样有时还是会感觉有点麻烦，可以使用.ssh/config文件进行配置：\n1 2 3 Host my_server_name HostName 192.168.1.114 User Javaer_12 接下来就可以通过输入ssh my_server_name来进行登录了。\n","date":"2026-01-14T17:07:17Z","permalink":"https://iamxurulin.github.io/p/%E5%9C%A8linux%E7%B3%BB%E7%BB%9F%E4%B8%AD%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8ssh%E8%BF%9B%E8%A1%8C%E8%BF%9C%E7%A8%8B%E7%99%BB%E5%BD%95/","title":"在Linux系统中如何使用ssh进行远程登录"},{"content":"在Linux系统中，可以使用systemctl命令来查看和管理系统服务。\nsystemctl list-units --type=service 查看所有服务的状态 systemctl status \u0026lt;服务名\u0026gt; 查看特定服务的状态 systemctl start \u0026lt;服务名\u0026gt; 启动服务 systemctl stop \u0026lt;服务名\u0026gt; 停止服务 systemctl restart \u0026lt;服务名\u0026gt; 重启服务 systemctl enable \u0026lt;服务名\u0026gt; 启用服务，设置为开机自启 systemctl disable \u0026lt;服务名\u0026gt; 禁用服务，取消开机自启 Linux 中服务名通常都是带.service后缀的（如nginx.service、redis-server.service），使用systemctl时省略.service 后缀，命令仍会生效。\n","date":"2026-01-14T16:46:44Z","permalink":"https://iamxurulin.github.io/p/%E6%95%B4%E7%90%86%E5%9C%A8linux%E7%B3%BB%E7%BB%9F%E4%B8%AD%E6%9F%A5%E7%9C%8B%E5%92%8C%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F%E6%9C%8D%E5%8A%A1%E7%9A%84%E5%91%BD%E4%BB%A4/","title":"整理在Linux系统中查看和管理系统服务的命令"},{"content":"整理了以下5种可能会触发Java的Full GC的情况：\n1.当老年代的空间不足并且无法通过老年代的垃圾回收以释放足够多的空间时，会触发Full GC来回收老年代中的对象。\n2.在Java 8以前，如果永久代的空间不足，会直接触发 Full GC。 在Java 8及以后，永久代被元空间取代，由于元空间手动设置了最大阈值，当内存耗尽时也可能会触发Full GC。\n3.当显示地调用System.gc()方法时，JVM可能并不能保证立即执行Full GC，但是有可能会触发。\n4.当新生代的to区放不下从eden区和from区拷贝过来的对象或者新生代的大对象或者长期存活的对象晋升到老年代时，如果老年代没有足够的空间来容纳这些对象，这个时候也会触发Full GC。\n5.在要进行新生代GC的时候，如果根据之前的统计数据发现新生代的平均晋升大小比现在的老年代剩余空间还要大，也会触发 Full GC。\n为了减少Full GC 的触发，可以考虑以下策略：\n调整堆内存的大小，从而减少老年代空间不足的情况 增大新生代的大小，从而减少对象晋升到老年代的频率 设置合理的元空间大小，从而避免元空间过小导致的频繁Full GC ","date":"2026-01-14T16:29:41Z","permalink":"https://iamxurulin.github.io/p/%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E4%BC%9A%E8%A7%A6%E5%8F%91java%E7%9A%84full-gc/","title":"什么情况下会触发Java的Full-GC"},{"content":"LangChain是基于链式结构的，像一个用于拼接模型、工具和记忆等组件的模块化AI应用框架，通过预定义步骤顺序执行，适合处理文档问答、简单客服这类线性任务。\nLangGraph是基于图结构的，像一个专注于流程控制和任务编排的有状态执行图框架，支持循环、分支和动态决策，适合临床试验审批、多智能体投资分析这类需要多角色协作、状态跟踪的复杂任务。\n在实际的开发过程中，一般简单任务用LangChain，复杂任务用LangGraph，对于特别复杂的任务则会考虑对这两者进行结合使用。\n","date":"2026-01-14T15:52:28Z","permalink":"https://iamxurulin.github.io/p/langchain%E5%92%8Clanggraph%E4%B8%A4%E8%80%85%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB/","title":"LangChain和LangGraph两者有什么区别"},{"content":" 求解代码 如果根节点的值和p,q的差值相乘是正数，说明这两个差值要么都是正数，要么都是负数，所以p、q肯定位于根节点的同一侧，需要继续往下找；\n如果相乘的结果是负数，说明p、q位于根节点的值两侧；\n如果相乘为0，说明p，q至少有一个是根节点的值。\n1 2 3 4 5 6 7 public int lowestCommonAncestor (TreeNode root, int p, int q) { while ((root.val-p)*(root.val-q)\u0026gt;0) { root = root.val\u0026gt;p?root.left:root.right; } return root.val; } ","date":"2026-01-14T15:06:51Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%9E%E9%80%92%E5%BD%92-%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E7%9A%84%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88/","title":"非递归-二叉搜索树的最近公共祖先"},{"content":"提示词的优化需要考虑以下5个维度：\n1.让模型清楚地知道要解决的是什么问题（目标明确）\n2.用分点、markdown/JSON/分隔符拆解任务，使提示词的结构更清晰（结构清晰）\n3.通过少量的输入输出样例告诉模型用户实际期望的结果到底长什么样，避免理解上的偏差（少量样本）\n4.给模型定义一个身份，让模型的输出风格更加符合实际的场景（角色）\n5.限制模型的输出范围，对字数、格式以及禁止的内容进行限定，避免模型出现幻觉（增加约束条件）\n提示词模板中的常见字段：\n角色定义 指定模型的身份 任务描述 用具体的指令多目标进行拆解 输入内容 提供原始数据或者问题案例 输出格式 规定结果的格式，比如输出markdown格式 约束规则 说明限制条件，比如回答不超过多少个字 评估标准 引导模型自检 ","date":"2026-01-14T15:06:27Z","permalink":"https://iamxurulin.github.io/p/%E6%8F%90%E7%A4%BA%E8%AF%8D%E7%9A%84%E4%BC%98%E5%8C%96%E9%9C%80%E8%A6%81%E8%80%83%E8%99%91%E5%93%AA%E4%BA%9B%E7%BB%B4%E5%BA%A6/","title":"提示词的优化需要考虑哪些维度"},{"content":" 求解代码 平衡二叉树的核心判断条件：\n当前节点的左右子树高度差小于等于1；\n当前节点的左右子树本身也是平衡二叉树；\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public boolean IsBalanced_Solution (TreeNode pRoot) { if(pRoot == null){ return true; } int left = depth(pRoot.left); int right = depth(pRoot.right); return Math.abs(left-right)\u0026lt;=1\u0026amp;\u0026amp;IsBalanced_Solution(pRoot.left)\u0026amp;\u0026amp;IsBalanced_Solution(pRoot.right); } public int depth(TreeNode root) { if(root == null){ return 0; } return Math.max(depth(root.left),depth(root.right))+1; } ","date":"2026-01-13T20:52:09Z","permalink":"https://iamxurulin.github.io/p/%E4%BB%8E%E4%B8%8A%E5%88%B0%E4%B8%8B-%E5%88%A4%E6%96%AD%E6%98%AF%E4%B8%8D%E6%98%AF%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91/","title":"从上到下-判断是不是平衡二叉树"},{"content":" 求解代码 不是完全二叉树主要就两种情况：\n1.有右节点无左节点\n2.如果是孩子不全的节点，则接下来必须全是叶子节点，否则就不是完全二叉树，对应设置一个leaf变量。\n队列双指针：left=队头(出队)，right=队尾(入队)\nleaf 变量的含义：\n是否已经进入【叶子节点阶段】，也可以理解为 后续所有节点都必须是「无孩子的叶子节点」，初始值false表示「还没到这个阶段」。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public static int MAXN = 101; public static TreeNode[] queue = new TreeNode[MAXN]; public static int left,right; public boolean isCompleteTree (TreeNode root) { if(root==null){ return true; } left=right=0; queue[right++]=root; boolean leaf = false; while (left\u0026lt;right) { root = queue[left++]; if(leaf\u0026amp;\u0026amp;(root.left!=null||root.right!=null)){ return false; } if(root.left==null\u0026amp;\u0026amp;root.right!=null){ return false; } if(root.left!=null){ queue[right++]=root.left; } if (root.right!=null) { queue[right++]=root.right; } if (root.left==null||root.right==null) { leaf = true; } } return true; } ","date":"2026-01-13T20:23:35Z","permalink":"https://iamxurulin.github.io/p/%E6%95%B0%E7%BB%84%E5%AE%9E%E7%8E%B0%E5%8F%8C%E7%AB%AF%E9%98%9F%E5%88%97-%E5%88%A4%E6%96%AD%E6%98%AF%E4%B8%8D%E6%98%AF%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91/","title":"数组实现双端队列-判断是不是完全二叉树"},{"content":"\n求解代码 之所以加上min和max，是因为二叉搜索树需要满足整棵左子树的所有节点都要小于根，整棵右子树的所有节点都要大于根。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public boolean isValidBST (TreeNode root) { return isValidBST(root, null, null); } boolean isValidBST(TreeNode root, TreeNode min, TreeNode max){ if(root == null){ return true; } if(min!=null\u0026amp;\u0026amp;root.val\u0026lt;=min.val){ return false; } if(max!=null\u0026amp;\u0026amp;root.val\u0026gt;=max.val){ return false; } return isValidBST(root.left,min,root)\u0026amp;\u0026amp;isValidBST(root.right,root,max); } ","date":"2026-01-13T19:03:28Z","permalink":"https://iamxurulin.github.io/p/%E9%80%92%E5%BD%92-%E5%88%A4%E6%96%AD%E6%98%AF%E4%B8%8D%E6%98%AF%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/","title":"递归-判断是不是二叉搜索树"},{"content":" 求解代码 这道题遍历二叉树的每一个节点，然后交换左右子节点就可以了。\n1.前序遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public TreeNode Mirror (TreeNode pRoot) { if(pRoot==null){ return null; } TreeNode temp = pRoot.left; pRoot.left = pRoot.right; pRoot.right = temp; Mirror(pRoot.left); Mirror(pRoot.right); return pRoot; } 2.中序遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public TreeNode Mirror (TreeNode pRoot) { if(pRoot==null){ return null; } Mirror(pRoot.left); TreeNode temp = pRoot.left; pRoot.left = pRoot.right; pRoot.right = temp; Mirror(pRoot.left);//注意上面交换过了 return pRoot; } 3.后序遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 public TreeNode Mirror (TreeNode pRoot) { if(pRoot==null){ return null; } TreeNode left = Mirror(pRoot.left); TreeNode right = Mirror(pRoot.right); pRoot.left = right; pRoot.right = left; return pRoot; } 注意⚠️：\n这里解释一下中序遍历的第二个递归为什么是Mirror(pRoot.left);而不是Mirror(pRoot.right);?\n因为执行完：\n1 2 3 TreeNode temp = pRoot.left; pRoot.left = pRoot.right; pRoot.right = temp; 这三行代码之后，当前节点的左右指针发生了互换，指向变成了：\n1 2 pRoot.left → 原右子树 (R) pRoot.right → 原左子树 (L) 要处理的「原右子树 R」，现在的内存地址其实是 pRoot.left，\n如果此时写 Mirror(pRoot.right)，实际处理的是「已经翻转完的原左子树 L」，这样就会造成翻转结果错误。\n","date":"2026-01-13T18:00:06Z","permalink":"https://iamxurulin.github.io/p/%E9%80%92%E5%BD%92-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E9%95%9C%E5%83%8F/","title":"递归-二叉树的镜像"},{"content":"强引用（Strong Reference） 这是最常见的引用类型。\n只要一个对象有强引用指向它，即便是系统内存紧张，垃圾回收器也不会回收该对象。\n软引用（Soft Reference） 软引用是用来描述一些还有用但是并非必需的对象，通常用于实现缓存机制，允许程序在不影响性能的情况下利用多余的内存。\n当系统内存不足时，垃圾回收器会对软引用指向的对象进行回收，避免内存溢出。\n弱引用（Weak Reference） 弱引用是比软引用更弱的一种引用类型，常用于防止内存泄露，允许缓存的键值对在不再使用的时候自动清除。\n和软引用在系统内存充足的情况下不会被回收不同的是，弱引用只要被垃圾回收器发现只有它指向某个对象时，不管系统内存是否充足，这个对象都会被回收。\n虚引用（Phanton Reference） 虚引用是最弱的一种引用类型，主要就是用来跟踪对象的垃圾回收状态。\n如果一个对象只有虚引用，那么这个对象随时会被垃圾回收器回收。\n虚引用必须和引用队列（ReferenceQueue）配合使用，否则虚引用就毫无意义。\n","date":"2026-01-13T16:57:03Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B7%E8%A7%A3%E9%87%8A%E4%B8%80%E4%B8%8Bjava%E4%B8%AD%E7%9A%84%E5%BC%BA%E5%BC%95%E7%94%A8%E5%BC%B1%E5%BC%95%E7%94%A8%E8%BD%AF%E5%BC%95%E7%94%A8%E5%92%8C%E8%99%9A%E5%BC%95%E7%94%A8/","title":"请解释一下Java中的强引用、弱引用、软引用和虚引用"},{"content":"Linux中的权限管理机制主要是围绕用户和组的权限展开。\nLinux中的文件和目录都是由：\n所有者（user）、组（Group）、其他人（Others）\n这三种实体来管理权限。\n可以使用ls -l命令来查看文件的权限和所有者。\n1.每个文件和目录都有一个所有者和一个所属组。\n每个用户都有一个唯一的用户ID（UID），每个组也有一个ID（GID）。\n一个用户可以属于多个组，但是一个文件只能有一个所有者和一个组属性。\n2.权限可以使用符号来表示，也可以使用八进制来表示。\n可读（r，read）、可写（w，write）、可执行（x，execute）\nr对应八进制4，w对应八进制2，x对应八进制1，通过相加和可得出最终的权限数值。\n","date":"2026-01-13T16:28:41Z","permalink":"https://iamxurulin.github.io/p/%E8%A7%A3%E9%87%8A%E4%B8%80%E4%B8%8Blinux%E7%B3%BB%E7%BB%9F%E7%9A%84%E6%9D%83%E9%99%90%E7%AE%A1%E7%90%86%E6%9C%BA%E5%88%B6/","title":"解释一下Linux系统的权限管理机制"},{"content":"主要可以从以下几个方面考虑：\n1.保证知识库中的原始文档内容准确、结构清晰、格式规范，尽量减少水印、不相关图片等噪音。\n2.由于过小的切片可能会导致语义不完整，过大的切片又可能会引入过多的无关信息，因此，需要采用合适的文档切片策略，避免固定的长度切分导致语义断裂。\n3.为了后续进行更精准地过滤和检索，可以考虑对文档切片添加来源、日期、类别、标签等元数据。\n4.使用大模型把用户的原始查询改写得更清晰、详细和规范一些，这样可以提高后续检索的准确性。\n5.像关键词检索和向量检索都有不同的优势，可以将两者进行结合，比如先用向量检索召回语义相关的文档，然后再使用关键词检索进行精确匹配。\n","date":"2026-01-13T16:06:35Z","permalink":"https://iamxurulin.github.io/p/%E6%80%8E%E4%B9%88%E4%BC%98%E5%8C%96rag%E7%9A%84%E6%A3%80%E7%B4%A2%E6%95%88%E6%9E%9C/","title":"怎么优化RAG的检索效果"},{"content":"要实现AI的多轮对话功能，关键在于让AI能够记住与用户之前的对话内容并保持上下文的连贯。\n可以使用Spring AI框架提供的对话记忆和Advisor特性来实现这个功能。\n主要是通过构造ChatClient来实现功能更丰富、更灵活的AI对话。\nChatClient可以看成是一系列可插拔的拦截器，在调用AI前后执行一些额外的操作。\nMessageChatMemoryAdvisor是实现多轮对话的关键Advisor，其主要作用就是从对话记忆中检索历史对话，然后将对话历史作为消息集合添加到当前的提示词中，这样的话，AI模型就可以记住之前进行过的交流。\nChatMemory接口中定义了保存消息、查询消息和清空历史的方法，MessageChatMemoryAdvisor也依赖于这个接口的实现来存取对话历史。\n为了解决对话记忆仅存在于内存中，在服务重启之后会造成记忆丢失的问题，需要考虑将对话记忆进行持久化。\n由于spring-ai-starter-model-chat-memory-jdbc的依赖版本较少，可以考虑自定义ChatMemory接口的方式实现：\n开发一个实现了ChatMemory接口的FileBasedChatMemory类，再使用高性能的Kryo序列化库将对话消息序列化后保存到本地文件中，读取的时候再进行反序列化。\n","date":"2026-01-13T15:49:02Z","permalink":"https://iamxurulin.github.io/p/%E6%80%8E%E4%B9%88%E5%AE%9E%E7%8E%B0ai%E7%9A%84%E5%A4%9A%E8%BD%AE%E5%AF%B9%E8%AF%9D%E5%8A%9F%E8%83%BD/","title":"怎么实现AI的多轮对话功能"},{"content":"试想一下这样一种场景：\n如果一个GPU集群的LLM处理能力为1000 tokens/s，那么1000个用户同时并发访问的话，响应给每个用户的性能只有 1 token/s吗？\n肯定不是。\n因为LLM并不是简单的线性分配资源，而是通过批处理与并发调度的方式来提升吞吐量的。\nLLM的核心计算是矩阵乘法，GPU的并行计算特性让“批量处理多个用户的tokens”耗时几乎不会增加，能充分地利用硬件资源。\n如果每一次批处理包含100个用户请求，每个用户10个tokens，那么1000个用户可以分10批处理完，当用户的性能是10 tokens/s。\n实际响应的速度取决于以下关键因素：\nToken的长度：输入Token影响批处理耗时，输出Token影响总响应时间，流式输出可以优化体感延迟；\n批处理策略：静态批处理简单并且易实现，动态批处理资源的利用率更高，连续批处理可以支撑超高并发；\n资源排队机制：FIFO、优先级队列等等策略决定请求的等待时间，不影响最终的处理速度。\n","date":"2026-01-13T15:28:15Z","permalink":"https://iamxurulin.github.io/p/%E6%80%8E%E4%B9%88%E5%88%86%E6%9E%90llm%E5%9C%A8%E5%B9%B6%E5%8F%91%E8%AE%BF%E9%97%AE%E6%97%B6%E7%9A%84%E6%80%A7%E8%83%BD%E7%93%B6%E9%A2%88/","title":"怎么分析LLM在并发访问时的性能瓶颈"},{"content":"将本地文件推送到github仓库时会遇到各种各样的问题，比如：\n为了解决这个GitHub 官方都承认的 HTTPS 不稳定问题，可以考虑使用ssh：\n① 生成 SSH Key（如果以前没配过） 在 任意目录 打开命令行窗口，执行：\n1 ssh-keygen -t ed25519 -C \u0026#34;你的GitHub邮箱\u0026#34; 一路 直接回车 × 3\n成功后会看到类似：\n1 Your identification has been saved in ... ② 复制 SSH 公钥（关键） 用windows的文本阅读器打开id_ed25519.pub文件，复制整行内容（以 ssh-ed25519 开头）\n③ GitHub 添加 SSH Key 登录 GitHub账户，右上角头像 → Settings→ SSH and GPG keys→ New SSH key\nTitle：任意起一个名字（如 my-ssh-key）\nKey：粘贴刚才复制的内容\n④ 测试 SSH 是否通 1 ssh -T git@github.com 第一次会问：\n1 Are you sure you want to continue connecting (yes/no)? 输入：\n1 yes 如果看到：\n1 Hi iamxurulin! You\u0026#39;ve successfully authenticated 说明 SSH 配置成功\n⑤接下来把仓库从 HTTPS 切到 SSH 在 public 目录执行：\n1 git remote set-url origin git@github.com:iamxurulin/iamxurulin.github.io.git 验证：\n1 git remote -v 可以看到：\n1 2 origin git@github.com:iamxurulin/iamxurulin.github.io.git (fetch) origin git@github.com:iamxurulin/iamxurulin.github.io.git (push) ⑥重新强制推送 1 git push -f origin main ","date":"2026-01-13T12:26:04Z","permalink":"https://iamxurulin.github.io/p/%E9%85%8D%E7%BD%AEssh%E8%A7%A3%E5%86%B3https%E4%B8%8D%E7%A8%B3%E5%AE%9A%E7%9A%84%E9%97%AE%E9%A2%98/","title":"配置ssh解决https不稳定的问题"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public TreeNode mergeTrees (TreeNode t1, TreeNode t2) { if(t1==null){ return t2; } if(t2==null){ return t1; } TreeNode head = new TreeNode(t1.val+t2.val); head.left = mergeTrees(t1.left, t2.left); head.right = mergeTrees(t1.right, t2.right); return head; } ","date":"2026-01-12T19:42:04Z","permalink":"https://iamxurulin.github.io/p/%E9%80%92%E5%BD%92-%E5%90%88%E5%B9%B6%E4%BA%8C%E5%8F%89%E6%A0%91/","title":"递归-合并二叉树"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 boolean recursion(TreeNode root1, TreeNode root2){ if(root1==null\u0026amp;\u0026amp;root2==null){ return true; } if(root1==null||root2==null||root1.val!=root2.val){ return false; } return recursion(root1.left, root2.right)\u0026amp;\u0026amp;recursion(root1.right, root2.left); } public boolean isSymmetrical (TreeNode pRoot) { return recursion(pRoot, pRoot); } 这里说明一下为什么要调用recursion(pRoot, pRoot);把根节点传两次：\n本质上就是把判断一棵树是否对称的问题转化为判断两棵树是否互为镜像的问题。\n第一颗树的遍历规则是：正常的根➡️左➡️右；\n第二棵树的遍历规则是：镜像的根➡️右➡️左；\n只要这两个遍历结果完全匹配，那么原树就是对称的。\n","date":"2026-01-12T19:29:57Z","permalink":"https://iamxurulin.github.io/p/%E9%80%92%E5%BD%92-%E5%AF%B9%E7%A7%B0%E7%9A%84%E4%BA%8C%E5%8F%89%E6%A0%91/","title":"递归-对称的二叉树"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public TreeNode head = null;//双向链表的头节点 public TreeNode cur = null;//双向链表的尾节点游标 public TreeNode Convert(TreeNode pRootOfTree) { if(pRootOfTree==null){ return null; } Convert(pRootOfTree.left); if(cur == null){ head = pRootOfTree; cur = pRootOfTree; }else{ cur.right = pRootOfTree; pRootOfTree.left = cur; cur = pRootOfTree; } Convert(pRootOfTree.right); return head; } 说明：\n核心就在于其中的if/else语句。\n1 2 if(cur为空) → 初始化链表，头和尾都是第一个节点； else → 把当前节点拼到链表尾部，完成双向绑定，然后移动尾指针。 ","date":"2026-01-12T18:57:53Z","permalink":"https://iamxurulin.github.io/p/%E9%80%92%E5%BD%92-%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E4%B8%8E%E5%8F%8C%E5%90%91%E9%93%BE%E8%A1%A8/","title":"递归-二叉搜索树与双向链表"},{"content":" 求解代码 检查遍历到的是否为叶子节点，并且当前的sum值等于节点值，如果是，说明恰好可以找到。\n1 2 3 4 5 6 7 8 9 10 11 public boolean hasPathSum (TreeNode root, int sum) { if(root==null){ return false; } if(root.left==null\u0026amp;\u0026amp;root.right==null\u0026amp;\u0026amp;sum-root.val==0){ return true; } return hasPathSum(root.left, sum-root.val)||hasPathSum(root.right, sum-root.val); } ","date":"2026-01-12T17:36:11Z","permalink":"https://iamxurulin.github.io/p/%E9%80%92%E5%BD%92-%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E5%92%8C%E4%B8%BA%E6%9F%90%E4%B8%80%E5%80%BC%E7%9A%84%E8%B7%AF%E5%BE%84-%E4%B8%80/","title":"递归-二叉树中和为某一值的路径-一"},{"content":"\n求解代码 1 2 3 4 5 6 7 public int maxDepth (TreeNode root) { if(root==null){ return 0; } return Math.max(maxDepth(root.left),maxDepth(root.right))+1; } ","date":"2026-01-12T17:15:19Z","permalink":"https://iamxurulin.github.io/p/%E9%80%92%E5%BD%92-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%A4%A7%E6%B7%B1%E5%BA%A6/","title":"递归-二叉树的最大深度"},{"content":"在Linux系统中，文件的权限可以通过chmod指令来设置，文件的拥有者可以通过chown 指令来设置。\n1.设置文件的权限\n1 chmod 755 my_file 设置my_file文件的权限为755\n2.设置文件的拥有者\n1 chown user:group my_file 将文件my_file的拥有者设置为user，所属的组设置为group\n在Linux中，文件权限以读（r）、写（w）、执行（x）三个基本权限来表示。\n权限分用户（u）、组（g）和其他用户（o）三类。\n755表示的是所有者拥有rwx权限，组合其他用户拥有rx权限，没有w权限。\n配置文件一般设置为644，脚本文件设置为755。\n","date":"2026-01-12T16:41:27Z","permalink":"https://iamxurulin.github.io/p/%E5%9C%A8linux%E7%B3%BB%E7%BB%9F%E4%B8%AD-%E5%A6%82%E4%BD%95%E8%AE%BE%E7%BD%AE%E6%96%87%E4%BB%B6%E7%9A%84%E6%9D%83%E9%99%90%E5%92%8C%E6%8B%A5%E6%9C%89%E8%80%85/","title":"在Linux系统中-如何设置文件的权限和拥有者"},{"content":"结构化输出是将大语言模型返回的自由文本输出转换为预定义的数据格式。\nSpring AI是通过StructuredOutputConverter机制来实现结构化输出的：\n1.StructuredOutputConverter实现了FormatProvider接口，这个接口提供特定的格式指令给AI模型，这些指令附加到用户的提示词后面，明确地告诉模型应该生成何种结构的输出。\n2.StructuredOutputConverter 实现了Spring的Converter\u0026lt;String, T\u0026gt;接口，这个接口负责将大模型返回的文本输出转换为开发者指定的目标类型。\nSpring AI提供了多种内置的转换器实现：\nBeanOutputConverter：转换为自定义Java实体类，在开发中最常用；\nMapOutputConverter：转换为松散的Map键值对结构；\nListOutputConverter：转换为指定类型的集合结构。\n","date":"2026-01-12T16:12:09Z","permalink":"https://iamxurulin.github.io/p/spring-ai%E6%80%8E%E4%B9%88%E5%AE%9E%E7%8E%B0%E7%BB%93%E6%9E%84%E5%8C%96%E8%BE%93%E5%87%BA/","title":"Spring-AI怎么实现结构化输出"},{"content":"Re-Reading（重读），是一种通过让大语言模型重新阅读问题来提高其推理能力的技术。\n有文献研究证明：\n对于复杂的问题，重复阅读和审视问题有助于模型更好地理解题意和约束，从而能够生成更准确、更深入的回答。\n在Spring AI中，可以通过自定义Advisor来实现Re-Reading功能：\n1.创建自定义Advisor类，同时实现用于同步请求的CallAroundAdvisor接口和用于流式请求的StreamAroundAdvisor接口，实现后可以兼容所有大模型调用场景。\n2.在Advisor的前置处理逻辑中，对用户的原始输入文本进行重读式Prompt增强改写。\n3.将改写后的提示词传递给大语言模型进行处理。\n","date":"2026-01-12T15:32:15Z","permalink":"https://iamxurulin.github.io/p/%E8%A7%A3%E9%87%8A%E4%B8%80%E4%B8%8Bre-reading/","title":"解释一下Re-Reading"},{"content":"在消息队列系统中，死信队列（Dead Letter Queue,DLQ）是一种处理无法正常消费的消息的机制。\nRabbitMQ 中实现死信机制的核心是死信交换机（Dead Letter Exchange, DLX），绑定该交换机的队列就是死信队列。\n死信队列是用来接收那些被拒绝的、过期的或者已经达到最大传递次数的消息。\n可以通过给队列设置x-dead-letter-exchange 参数来指定死信交换机，搭配x-dead-letter-routing-key指定死信路由键，从而实现死信队列的绑定。\n","date":"2026-01-12T14:44:12Z","permalink":"https://iamxurulin.github.io/p/%E4%BB%80%E4%B9%88%E6%98%AFrabbitmq%E4%B8%AD%E7%9A%84%E6%AD%BB%E4%BF%A1%E9%98%9F%E5%88%97/","title":"什么是RabbitMQ中的死信队列"},{"content":"TTL（Time To Live）表示消息在队列中存活的时间，主要用于防止消息在队列中无限积压，导致系统资源的耗尽。\n配置TTL有两种方式，一种是队列级别的TTL，另外一种是消息级别的TTL。\n1.在声明队列时通过设置x-message-ttl参数来指定队列中所有消息的TTL。\n2.在发送消息时通过AMQP.BasicProperties属性指定单个消息的TTL。\n","date":"2026-01-12T14:17:02Z","permalink":"https://iamxurulin.github.io/p/%E6%80%8E%E4%B9%88%E5%9C%A8rabbitmq%E4%B8%AD%E9%85%8D%E7%BD%AE%E6%B6%88%E6%81%AF%E7%9A%84ttl/","title":"怎么在RabbitMQ中配置消息的TTL"},{"content":" 求解代码 在【队列】求二叉树的层序遍历的基础上增加一个flag区分奇偶性就行。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; Print (TreeNode pRoot) { ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); if(pRoot==null){ return ans; } Queue\u0026lt;TreeNode\u0026gt; queue = new ArrayDeque\u0026lt;TreeNode\u0026gt;(); queue.add(pRoot); boolean flag = true; while(!queue.isEmpty()){ ArrayList\u0026lt;Integer\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); int n = queue.size(); flag = !flag; for(int i=0;i\u0026lt;n;i++){ TreeNode cur = queue.poll(); list.add(cur.val); if (cur.left!=null) { queue.add(cur.left); } if(cur.right!=null){ queue.add(cur.right); } } if(flag){ Collections.reverse(list); } ans.add(list); } return ans; } ","date":"2026-01-11T19:32:36Z","permalink":"https://iamxurulin.github.io/p/%E9%98%9F%E5%88%97-%E6%8C%89%E4%B9%8B%E5%AD%97%E5%BD%A2%E9%A1%BA%E5%BA%8F%E6%89%93%E5%8D%B0%E4%BA%8C%E5%8F%89%E6%A0%91/","title":"队列-按之字形顺序打印二叉树"},{"content":"\n求解代码 构建一个辅助队列queue，让root首先进入队列。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; levelOrder (TreeNode root) { ArrayList\u0026lt;ArrayList\u0026lt;Integer\u0026gt;\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); if(root==null){ return ans; } Queue\u0026lt;TreeNode\u0026gt; queue = new ArrayDeque\u0026lt;TreeNode\u0026gt;(); queue.add(root); while(!queue.isEmpty()){ ArrayList\u0026lt;Integer\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); int n = queue.size(); for(int i=0;i\u0026lt;n;i++){ TreeNode cur = queue.poll(); list.add(cur.val); if (cur.left!=null) { queue.add(cur.left); } if(cur.right!=null){ queue.add(cur.right); } } ans.add(list); } return ans; } ","date":"2026-01-11T19:12:25Z","permalink":"https://iamxurulin.github.io/p/%E9%98%9F%E5%88%97-%E6%B1%82%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86/","title":"队列-求二叉树的层序遍历"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public void postorder(List\u0026lt;Integer\u0026gt; list, TreeNode root){ if(root==null){ return; } postorder(list, root.left); postorder(list, root.right); list.add(root.val); } public int[] postorderTraversal (TreeNode root) { List\u0026lt;Integer\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); postorder(list, root); int[] ans = new int[list.size()]; for(int i=0;i\u0026lt;list.size();i++){ ans[i]=list.get(i); } return ans; } 说明：\n和前文【递归】二叉树的前序遍历的区别就是把list.add(root.val); 这行代码放到了两个递归之后。\n总结一下：\n前序遍历，list.add(root.val); 这行代码放到两个递归之前。 中序遍历，list.add(root.val); 这行代码放到两个递归之间。 后序遍历，list.add(root.val); 这行代码放到两个递归之后。 二叉树递归遍历的灵魂就在于list.add(root.val); 的位置决定遍历的方式，而递归的逻辑完全不变。\n","date":"2026-01-11T17:57:22Z","permalink":"https://iamxurulin.github.io/p/%E9%80%92%E5%BD%92-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%90%8E%E5%BA%8F%E9%81%8D%E5%8E%86/","title":"递归-二叉树的后序遍历"},{"content":" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public void inorder(List\u0026lt;Integer\u0026gt; list, TreeNode root){ if(root==null){ return; } inorder(list, root.left); list.add(root.val); inorder(list, root.right); } public int[] inorderTraversal (TreeNode root) { List\u0026lt;Integer\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); inorder(list, root); int[] ans = new int[list.size()]; for(int i=0;i\u0026lt;list.size();i++){ ans[i]=list.get(i); } return ans; } 说明：\n和前文【递归】二叉树的前序遍历的区别就是把list.add(root.val); 这行代码放到了两个递归之间。\n二叉树递归遍历的灵魂就在于list.add(root.val); 的位置决定遍历的方式，而递归的逻辑完全不变。\n","date":"2026-01-11T17:46:53Z","permalink":"https://iamxurulin.github.io/p/%E9%80%92%E5%BD%92-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86/","title":"递归-二叉树的中序遍历"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public void preorder(List\u0026lt;Integer\u0026gt; list, TreeNode root){ if(root==null){ return; } list.add(root.val); preorder(list, root.left); preorder(list, root.right); } public int[] preorderTraversal (TreeNode root) { List\u0026lt;Integer\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); preorder(list, root); int[] ans = new int[list.size()]; for(int i=0;i\u0026lt;list.size();i++){ ans[i]=list.get(i); } return ans; } ","date":"2026-01-11T17:36:19Z","permalink":"https://iamxurulin.github.io/p/%E9%80%92%E5%BD%92-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86/","title":"递归-二叉树的前序遍历"},{"content":"在RabbitMQ中，prefetch参数主要是用于限制消费者端可以同时预取并未确认消息的最大数量，帮助消费者端控制消息处理的流量。\n举个例子说明一下：\n如果prefetch的参数设置为1，那么消费者会在未确认消息之前，只预取一个消息，这样可以确保每个消息在被处理成功之后，才接收下一个消息，从而避免消息的积压和资源耗尽。\n在负载均衡、避免内存溢出以及优先级处理这些场景下会用到prefetch参数的设置。\n比如：\n1.RabbitMQ默认轮询分发消息，消费者可以根据其处理能力进行动态地调整prefetch值，实现基于处理能力的动态负载均衡。\n2.如果有大量未处理的消息堆积在内存中可能会导致内存溢出，这时通过设置一个较小的prefetch值可以避免这种情况。\n3.配合RabbitMQ的优先级队列，设置prefetch值可以确保队列优先推送高优先级的消息，让其被及时处理。\n注意⚠️：\nprefetch仅在消费者手动确认消息模式下生效。\n","date":"2026-01-11T17:07:58Z","permalink":"https://iamxurulin.github.io/p/rabbitmq%E4%B8%AD%E7%9A%84prefetch%E5%8F%82%E6%95%B0/","title":"RabbitMQ中的Prefetch参数"},{"content":"JVM的内存区域指的是JVM的运行时数据区。\n主要分为方法区、堆、虚拟机栈、本地方法栈和程序计数器五个主要区域。\n1.方法区（Method Area）\n线程共享区域，存着类的结构信息、常量、静态变量。\n2.堆（Heap）\n最大的共享区，专门放对象和数组。\n3.虚拟机栈（JVM Stack）\n线程私有，存着局部变量、操作数栈、动态链接、方法出口信息、基本类型变量、对象引用。\n4.本地方法栈（Native Method Stack）\n线程私有，用来分配内存给非Java方法。\n类似于虚拟机栈，专为JNI调用本地代码服务。\n5.程序计数器（Program Counter Register）\n每个线程都有一个独立的程序计数器，线程私有。\n保存当前线程执行的Java 方法字节码指令的地方或者行号。\n","date":"2026-01-11T16:36:51Z","permalink":"https://iamxurulin.github.io/p/jvm%E7%9A%84%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E6%98%AF%E6%80%8E%E4%B9%88%E5%88%92%E5%88%86%E7%9A%84/","title":"JVM的内存区域是怎么划分的"},{"content":"JIT（Just-In-Time）编译后的代码通常存放在代码缓存区（Code Cache）中。\nJVM提供了如下参数用于调整Code Cache的大小和行为：\n-XX:InitialCodeCacheSize 初始大小 -XX:ReservedCodeCacheSize 最大大小 -XX:+PrintCodeCache 打印Code Cache信息 Code Cache的默认大小依赖于JVM的版本和运行环境，通常有一个最大值。\nCode Cache分为多个区域，分别存储不同级别的编译代码：\n非方法代码 存储运行时的JVM调试代码或者模板代码 方法代码 存储普通JIT编译的代码 轮廓代码 存储优化级别更高的代码 ","date":"2026-01-11T16:04:17Z","permalink":"https://iamxurulin.github.io/p/jit%E7%BC%96%E8%AF%91%E5%90%8E%E7%9A%84%E4%BB%A3%E7%A0%81%E5%AD%98%E5%9C%A8%E5%93%AA/","title":"JIT编译后的代码存在哪"},{"content":"Spring AI是一个基于Spring生态的AI应用开发框架。\n通过提供统一的API和抽象，让Java开发者可以不用考虑底层实现的差异，更便捷地接入和使用各种AI大模型及其相关技术。\nSpring AI框架的核心特性：\n1.为聊天、文本转图像和嵌入模型提供统一的API，支持流式调用和同步，支持访问特定模型的功能。\n2.支持OpenAI、微软Azure、Google、Ollama在内的主流AI模型供应商。\n3.可以实现将AI模型的输出自动映射到POJO，方便在Java应用中处理。\n4.支持与多种主流向量数据库的集成，通过跨向量存储的可移植API。\n5.支持模型请求执行客户端定义的函数和工具。\n6.提供文档抽取、转换和加载的组件，可用于数据工程和RAG知识库的构建。\n7.为AI模型和向量存储提供了自动配置和Starter依赖。\n8.提供类似于WebClient和RestClient的流式API，便于与AI模型交互。\n9.提供标准化的Prompt模板引擎，支持动态参数填充和模板复用。\n","date":"2026-01-11T15:43:59Z","permalink":"https://iamxurulin.github.io/p/%E4%BB%8B%E7%BB%8D%E4%B8%80%E4%B8%8Bspring-ai%E6%A1%86%E6%9E%B6/","title":"介绍一下Spring-AI框架"},{"content":"LangGraph的编排原理是通过图结构将一个复杂的AI任务分解为可编排的节点，通过状态流转和条件边实现动态流程控制。\n有节点、边、状态三个核心要素：\n1.节点代表独立处理单元，每个节点负责接收状态并返回更新后的状态。\n2.边定义节点间的流转路径，支持条件分支和循环。\n3.状态贯穿整个流程的上下文数据，驱动节点间的动态交互。\n通过画图描述逻辑任务，框架自动根据状态流转执行节点，支持复杂的多Agent协作和动态决策。\n","date":"2026-01-11T14:57:27Z","permalink":"https://iamxurulin.github.io/p/langgraph%E7%9A%84%E7%BC%96%E6%8E%92%E5%8E%9F%E7%90%86/","title":"LangGraph的编排原理"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public int compare (String version1, String version2) { String[] str1 = version1.split(\u0026#34;\\\\.\u0026#34;); String[] str2 = version2.split(\u0026#34;\\\\.\u0026#34;); int len1 = str1.length; int len2 = str2.length; int len =len1\u0026gt;len2?len1:len2; for(int i=0;i\u0026lt;len;i++){ int val1 = i\u0026lt;len1?Integer.parseInt(str1[i]):0; int val2 = i\u0026lt;len2?Integer.parseInt(str2[i]):0; if(val1\u0026gt;val2){ return 1; }else if(val1\u0026lt;val2){ return -1; } } return 0; } ","date":"2026-01-10T22:26:28Z","permalink":"https://iamxurulin.github.io/p/%E6%AF%94%E8%BE%83%E7%89%88%E6%9C%AC%E5%8F%B7/","title":"比较版本号"},{"content":"\n求解代码 对旋转数组来说，右子数组的数值整体更小，左子数组的数值整体更大。\n数组的最小值一定是右子数组的第一个元素。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public int minNumberInRotateArray (int[] nums) { if(nums.length==0){ return 0; } int i=0; int j=nums.length-1; while(i\u0026lt;j){ int mid = i+((j-i)\u0026gt;\u0026gt;1); if(nums[mid]\u0026gt;nums[j]){ i=mid+1; }else if(nums[mid]\u0026lt;nums[j]){ j=mid; }else{ j--; } } return nums[i]; } ","date":"2026-01-10T22:02:56Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%88%86%E6%B3%95-%E6%97%8B%E8%BD%AC%E6%95%B0%E7%BB%84%E7%9A%84%E6%9C%80%E5%B0%8F%E6%95%B0%E5%AD%97/","title":"二分法-旋转数组的最小数字"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 int mod = 1000000007; long mergeSort(int[] nums, int left, int right) { if (left \u0026gt;= right) { return 0; } int mid = left + ((right - left) \u0026gt;\u0026gt; 1); // 分治计算左右区间，并立即取模 long res = (mergeSort(nums, left, mid) + mergeSort(nums, mid + 1, right)) % mod; int[] temp = new int[right - left + 1]; int i = left; int j = mid + 1; int k = 0; while (i \u0026lt;= mid \u0026amp;\u0026amp; j \u0026lt;= right) { // 严格判断nums[i] \u0026lt;= nums[j]，仅大于时统计逆序对 if (nums[i] \u0026lt;= nums[j]) { temp[k++] = nums[i++]; } else { temp[k++] = nums[j++]; // 累加后立即取模，防止溢出 res = (res + mid - i + 1) % mod; } } // 拷贝左区间剩余元素 while (i \u0026lt;= mid) { temp[k++] = nums[i++]; } // 拷贝右区间剩余元素 while (j \u0026lt;= right) { temp[k++] = nums[j++]; } // 覆盖回原数组 for (k = 0, i = left; k \u0026lt; temp.length; k++, i++) { nums[i] = temp[k]; } return res; } public int InversePairs(int[] nums) { if (nums == null || nums.length \u0026lt;= 1) { return 0; } return (int) (mergeSort(nums, 0, nums.length - 1) % mod); } ","date":"2026-01-10T21:38:31Z","permalink":"https://iamxurulin.github.io/p/%E5%BD%92%E5%B9%B6-%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E9%80%86%E5%BA%8F%E5%AF%B9/","title":"归并-数组中的逆序对"},{"content":" 求解代码 因为本题假设nums[-1]=nums[n]=-∞，所以，只要数组中存在一个元素比相邻的元素大，那么沿着它就一定可以找到一个峰值。\n使用二分查找，比较mid和mid+1位置的值，如果mid位置的值更大，则左侧存在峰值；\n如果mid+1位置的值更大，则右侧存在峰值。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 public int findPeakElement (int[] nums) { int left = 0; int right = nums.length; while(left\u0026lt;right){ int mid = left+((right-left)\u0026gt;\u0026gt;1); if(nums[mid]\u0026gt;nums[mid+1]){ right=mid; }else{ left=mid+1; } } return left; } 注意⚠️：\n这里说明一下为什么无序数组也能使用二分查找，主要是这道题通过比较nums[mid]和nums[mid+1]，可以确定峰值的位置区间，这满足二分的核心条件，所以无序也能用二分。\n","date":"2026-01-10T18:42:01Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%88%86%E6%B3%95-%E5%AF%BB%E6%89%BE%E5%B3%B0%E5%80%BC/","title":"二分法-寻找峰值"},{"content":"\n求解代码 这道题因为每一行从左到右都是递增的，且每一列从上到下也都是递增的。\n我们不难发现，左上角是最小值，右下角是最大值。\n左下的元素大于它上方的元素，小于它右方的元素。\n所以，我们可以以左下角为起点，如果它小于target，则往右移找更大的；\n如果它大于target，则往上移找更小的。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public boolean Find (int target, int[][] array) { if(array.length==0||array[0].length==0){ return false; } int n = array.length; int m = array[0].length; for(int i=n-1,j=0;i\u0026gt;=0\u0026amp;\u0026amp;j\u0026lt;m;){ if(array[i][j]\u0026lt;target){ j++; }else if(array[i][j]==target){ return true; }else{ i--; } } return false; } ","date":"2026-01-10T17:29:11Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%88%86%E6%B3%95-%E4%BA%8C%E7%BB%B4%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E6%9F%A5%E6%89%BE/","title":"二分法-二维数组中的查找"},{"content":"Java中的常量池是一块用于存储运行时常量或者符号的区域。\n主要有字符串常量池和运行时常量池。\n字符串常量池用于存储字符串字面量，可以通过String类中的intern()方法复用常量池中的字符串对象，若不存在则将当前字符串对象入池。\nJava6中的字符串常量池位于方法区中的永久代中，Java7及以后位于堆内存中的特殊区域。\n在Java中，字符串的创建方式有两种，一种是直接使用字面量，另一种是使用new关键字。\n运行时常量池存储的是每个类或者接口的Class文件编译时生成的常量信息。\n需要说明的是，按照JVM的定义来说，字符串常量池逻辑上还是属于运行时常量池，只是因为字符串的高频使用，被单独抽离出来做了优化设计。\n","date":"2026-01-10T17:02:58Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4java%E4%B8%AD%E7%9A%84%E5%B8%B8%E9%87%8F%E6%B1%A0/","title":"说说Java中的常量池"},{"content":"RabbitMQ的消息确认机制主要是用于确保消息的可靠传递，防止消息丢失。\n通过发布确认和消费者确认来实现。\n发布确认 当生产者发送消息到RabbitMQ时，可以选择开启发布确认模式，当RabbitMQ成功将消息入队/持久化之后，会发送一个ACK回复生产者，告知消息成功到达队列； 仅当消息处理失败时会返回NACK。\n如果生产者在合理的时间内没有收到ACK或者NACK，则可以重发消息。\n消费者确认 消费者在处理完一条消息之后，必须发送一个ACK给RabbitMQ，告知RabbitMQ该消息已经处理完成，RabbitMQ在收到ACK之后会将该消息从队列中永久删除。\n如果RabbitMQ在收到消费者ACK之前检测到消费者已经断开连接，则认为该消息没有被成功处理，RabbitMQ会重新发送给其他消费者消费。\n","date":"2026-01-10T16:33:38Z","permalink":"https://iamxurulin.github.io/p/rabbitmq%E7%9A%84%E6%B6%88%E6%81%AF%E7%A1%AE%E8%AE%A4%E6%9C%BA%E5%88%B6%E6%98%AF%E6%80%8E%E4%B9%88%E5%B7%A5%E4%BD%9C%E7%9A%84/","title":"RabbitMQ的消息确认机制是怎么工作的"},{"content":" public boolean equals(Object obj) 用来比较两个对象是不是相等，默认比较的是内存地址。 public int hashCode() 返回当前对象的哈希值 public String toString() 返回对象的字符串形式 public final Class\u003c?\u003e getClass() 返回对象运行时的类信息，反射的时候经常用到 public void notify() 唤醒一个正在等待当前对象的线程。必须在synchronized代码块中配合wait()一起使用 public void notifyAll() 唤醒所有在这个对象上等待的线程 public void wait() 当前线程挂起，等待别的线程来notify它，只能在同步块/同步方法中使用 public void wait(long timeout) 在wait方法的基础上加了个超时控制，过了时间就自己醒来 public void wait(long timeout,int nanos) 在wait()方法的基础上加了个更精细的超时控制，支持纳秒级的等待 protected Object clone() 拷贝当前对象，默认是浅拷贝 protected void finalize() 对象被垃圾回收之前会自动调用这个方法 ","date":"2026-01-10T15:47:23Z","permalink":"https://iamxurulin.github.io/p/%E6%95%B4%E7%90%86java%E4%B8%ADobject%E7%B1%BB%E7%9A%84%E6%96%B9%E6%B3%95%E5%92%8C%E4%BD%9C%E7%94%A8/","title":"整理Java中Object类的方法和作用"},{"content":"LangChain的核心架构主要由LangChain Libraries、LangChain Templates、LangServe和LangSmith四个模块组成。\nLangChain Libraries 整个LangChain框架的基础，包含langchain-core、langchain以及langchain-community三个子模块。\n其中，langchain-core提供了构建应用所需的核心功能；\nlangchain是构建链和代理的主要模块；\nlangchain-community整合了多个第三方库和集成。\nLangChain Templates 提供了一系列适用于各种任务的参考架构模板，像问答系统、文档解析和对话管理等常见的任务。\nLangServe 用于将LangChain构建的链部署为REST API的库，支持高并发请求和流式操作，适用于构建生产环境中的API服务。\nLangSmith 这是一个开发者平台，提供调试、测试、评估和监控的功能。\n","date":"2026-01-10T15:22:25Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4langchain%E7%9A%84%E6%A0%B8%E5%BF%83%E6%9E%B6%E6%9E%84/","title":"说说LangChain的核心架构"},{"content":"LangChain是一个专为大语言模型应用开发而设计的框架。\n主要有以下6个核心组件：\n1.模型集成。支持OpenAI、Anthropic、Llama等多种语言模型，提供了统一的接口，可以在不同模型之间切换。\n2.提示词模板。通过模板化的方式，根据不同的输入生成相应的提示词，引导模型生成更加准确的输出。\n3.记忆机制。通过存储对话的上下文信息，使得LangChain能够在多轮对话中保持上下文的一致性，提升模型的响应质量。\n4.链式调用。将多个处理步骤串联起来，形成一个处理流程，使复杂任务的处理更加模块化和可复用。\n5.智能体。Agent可以根据用户的输入动态地选择合适的工具来完成任务，实现更加灵活的任务处理。\n6.工具集成。集成各种外部工具，提供访问外部资源的能力，扩展了模型的功能，使其能够处理更复杂的任务。\n","date":"2026-01-10T14:45:10Z","permalink":"https://iamxurulin.github.io/p/langchain%E6%9C%89%E5%93%AA%E4%BA%9B%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6/","title":"LangChain有哪些核心组件"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public int search (int[] nums, int target) { // 左指针：起始下标 int left = 0; // 右指针：结束下标，闭区间 [left, right] int right = nums.length - 1; while (left \u0026lt;= right) { int mid = left + ((right - left) \u0026gt;\u0026gt; 1); if (nums[mid] \u0026lt; target) { // 目标值在右半区，左指针右移 left = mid + 1; } else if (nums[mid] == target) { // 找到目标值，返回对应下标 return mid; } else { // 目标值在左半区，右指针左移 right = mid - 1; } } // 遍历完无目标值，返回-1 return -1; } ","date":"2026-01-09T22:09:01Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE-i/","title":"二分查找-I"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public ListNode deleteDuplicates (ListNode head) { // 空链表 或 单节点链表，无重复节点，直接返回 if(head == null || head.next == null){ return head; } // 虚拟头节点 ListNode dummy = new ListNode(0); dummy.next = head; ListNode cur = head; // 游标指针，遍历链表找重复节点 ListNode pre = dummy; // 前驱指针，锚定无重复的有效节点 while(cur != null){ // 找到连续重复节点的最后一个节点 while(cur.next != null \u0026amp;\u0026amp; cur.val == cur.next.val){ cur = cur.next; } if(pre.next == cur){ pre = pre.next; // 从 pre 到 cur，中间没有跳过节点 }else{ pre.next = cur.next; // 从 pre 到 cur，中间跳过了节点 } cur = cur.next; // 游标指针后移，继续遍历 } // 返回新链表的有效头节点 return dummy.next; } 说明：\npre是永远站在「已经确认无重复的最后一个节点」的位置。\n","date":"2026-01-09T21:50:41Z","permalink":"https://iamxurulin.github.io/p/%E5%8F%8C%E6%8C%87%E9%92%88-%E5%88%A0%E9%99%A4%E6%9C%89%E5%BA%8F%E9%93%BE%E8%A1%A8%E4%B8%AD%E9%87%8D%E5%A4%8D%E7%9A%84%E5%85%83%E7%B4%A0-ii/","title":"双指针-删除有序链表中重复的元素-II"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public ListNode deleteDuplicates (ListNode head) { // 空链表 或 单节点链表，无重复节点，直接返回 if(head == null || head.next == null){ return head; } // 定义游标指针，从链表头节点开始遍历 ListNode cur = head; // 遍历链表，直到当前节点是最后一个节点 while(cur.next != null){ // 当前节点和下一个节点值相等，删除下一个重复节点 if(cur.val == cur.next.val){ cur.next = cur.next.next; }else{ // 值不相等，指针正常后移 cur = cur.next; } } // 返回原链表头节点 return head; } ","date":"2026-01-09T21:14:50Z","permalink":"https://iamxurulin.github.io/p/%E5%8D%95%E6%8C%87%E9%92%88-%E5%88%A0%E9%99%A4%E6%9C%89%E5%BA%8F%E9%93%BE%E8%A1%A8%E4%B8%AD%E9%87%8D%E5%A4%8D%E7%9A%84%E5%85%83%E7%B4%A0-i/","title":"单指针-删除有序链表中重复的元素-I"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public ListNode oddEvenList (ListNode head) { // 空链表 或 单节点链表，直接返回原链表 if(head == null || head.next == null){ return head; } // 初始化奇数链表的头节点和游标 ListNode oddHead = head; ListNode oddCur = oddHead; // 初始化偶数链表的头节点和游标 ListNode evenHead = head.next; ListNode evenCur = evenHead; while(evenCur != null \u0026amp;\u0026amp; evenCur.next != null){ oddCur.next = oddCur.next.next; // 奇数节点跳过偶数位，指向下一个奇数位 evenCur.next = evenCur.next.next; // 偶数节点跳过奇数位，指向下一个偶数位 oddCur = oddCur.next; // 奇数游标后移 evenCur = evenCur.next; // 偶数游标后移 } // 拼接奇数链表和偶数链表 oddCur.next = evenHead; // 返回重排后的链表头节点 return oddHead; } ","date":"2026-01-09T21:00:23Z","permalink":"https://iamxurulin.github.io/p/%E9%93%BE%E8%A1%A8%E7%9A%84%E5%A5%87%E5%81%B6%E9%87%8D%E6%8E%92/","title":"链表的奇偶重排"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public boolean isPail (ListNode head) { // 空链表 或 单节点链表 一定是回文链表 if (head == null || head.next == null) { return true; } ListNode fast = head; ListNode slow = head; // 找链表中点：快指针走2步，慢指针走1步 while (fast != null \u0026amp;\u0026amp; fast.next != null) { fast = fast.next.next; slow = slow.next; } // 链表长度为奇数时，跳过正中间的节点 if (fast != null) { slow = slow.next; } // 快指针重置为链表头，慢指针指向反转后的后半段链表头 fast = head; slow = reverseList(slow); // 双指针逐一比对前后两段链表的节点值 while (slow != null) { if (slow.val != fast.val) { return false; } slow = slow.next; fast = fast.next; } // 所有节点值都相等，是回文链表 return true; } // 反转链表 public ListNode reverseList(ListNode head) { ListNode pre = null; ListNode cur = head; ListNode next = null; while (cur != null) { next = cur.next; // 保存下一个节点 cur.next = pre; // 反转当前节点的指针指向 pre = cur; // 前驱节点向后移动 cur = next; // 当前节点向后移动 } return pre; // 返回反转后的链表头节点 } ","date":"2026-01-09T20:22:28Z","permalink":"https://iamxurulin.github.io/p/%E5%88%A4%E6%96%AD%E4%B8%80%E4%B8%AA%E9%93%BE%E8%A1%A8%E6%98%AF%E5%90%A6%E4%B8%BA%E5%9B%9E%E6%96%87%E7%BB%93%E6%9E%84/","title":"判断一个链表是否为回文结构"},{"content":"主要是采用引用计数法和可达性分析算法两种方式来实现。\n引用计数法（Reference Counting） 每个对象维护一个引用计数器，当引用计数增加时，计数器加1，减少时，计数器减1。\n当引用计数器为0时，说明该对象不再被引用，此时可以被回收。\n这种方法实现起来比较简单而且实时性好，但是无法处理循环引用的问题。\n可达性分析算法（Reachability Analysis） 这是Java中的垃圾回收主要采用的方法。\n通过一组成为“GC Roots”的对象出发，遍历所有可达的对象，对于一切无法通过GC Roots到达的对象，均可视为垃圾。\n这种方法可以解决循环引用的问题，但是需要消耗一定的资源进行标记。\n","date":"2026-01-09T16:00:00Z","permalink":"https://iamxurulin.github.io/p/java%E4%B8%AD%E6%80%8E%E4%B9%88%E5%88%A4%E6%96%AD%E5%AF%B9%E8%B1%A1%E6%98%AF%E5%90%A6%E6%98%AF%E5%9E%83%E5%9C%BE/","title":"Java中怎么判断对象是否是垃圾"},{"content":"检查系统的磁盘使用情况可以使用df和du这两个命令。\ndf命令 df（disk filesystem），用于查看磁盘文件系统的整体使用情况。\n1 df -h h表示human-readable，就是以人类可读的格式输出磁盘分区、已用空间、空闲空间、总空间和挂载点的信息。\ndu命令 du（disk usage），用于检查指定目录及其子目录的磁盘使用情况。\n1 du -sh /home/user -s显示目录的总大小，-h以人类可读的格式显示，合起来就是以人类可读的格式查看home/user目录的总大小。\n1 du -a 显示目录和所有文件的大小。\n","date":"2026-01-09T15:15:00Z","permalink":"https://iamxurulin.github.io/p/%E5%9C%A8linux%E7%B3%BB%E7%BB%9F%E4%B8%AD-%E6%80%8E%E4%B9%88%E6%A3%80%E6%9F%A5%E7%B3%BB%E7%BB%9F%E7%9A%84%E7%A3%81%E7%9B%98%E4%BD%BF%E7%94%A8%E6%83%85%E5%86%B5/","title":"在Linux系统中-怎么检查系统的磁盘使用情况"},{"content":"NIO（Non-blocking I/O），非阻塞I/O模式，调用方在发起I/O操作后即使操作未完成，也能立即返回。\n结合I/O多路复用技术，可以使一个线程同时管理多个连接。\n适用于连接数多、高并发和高性能要求的场景。\nBIO（Blocking I/O），传统的阻塞式I/O模式，调用方在发起I/O操作时会被阻塞，直到操作完成后才会继续执行。\n适用于连接数较少，逻辑简单的场景。\nAIO（Asynchronous I/O）异步I/O模式，调用方在发起I/O请求后，不需要轮询或者等待I/O操作完成，可以继续执行其他任务，操作系统或者底层库会在I/O操作完成后，通过回调或者事件通知的方式告知调用方。\n适用于对响应时间要求较高的应用场景。\n","date":"2026-01-09T14:45:00Z","permalink":"https://iamxurulin.github.io/p/%E8%A7%A3%E9%87%8A%E4%B8%80%E4%B8%8Bniobioaio/","title":"解释一下NIO、BIO、AIO"},{"content":"Selector Selector是Java Non-blocking I/O中用于实现I/O多路复用的组件。\nSelector有4种事件类型，分别是：\nOP_READ 表示通道中有数据可读 OP_WRITE 表示可以向通道中写入数据 OP_CONNECT 表示通道完成连接操作 OP_ACCEPT 表示通道可以接受新的连接 Selector的作用有两个：\n一个是通过一个Selector实例，程序可以同时监听多个通道的I/O事件。\n另一个是Selector通常与非阻塞通道配合使用，可以实现高效地非阻塞I/O操作。\nChannel Channel是Java Non-blocking I/O中的一个核心概念，主要用于数据的读写操作。\nChannel有四种类型：\nSocketChannel 用于基于TCP的网络通信，可以与服务器或者客户端进行连接 ServerSocketChannel 仅用于TCP服务端， 用于监听TCP连接 DatagramChannel 用于基于UDP的网络通信 FileChannel 用于从文件中读取或者向文件中写入数据 Channel是双向的，可以同时支持读取和写入。\n传统的流要么是输入流，要么是输出流，只能是单向的。\n因此，Channel比传统的I/O流更灵活和高效。\nChannel可以结合Selector实现多路复用，从而处理多个并发连接。\n","date":"2026-01-09T14:15:00Z","permalink":"https://iamxurulin.github.io/p/%E8%A7%A3%E9%87%8A%E4%B8%80%E4%B8%8Bselectorchannel/","title":"解释一下Selector、Channel"},{"content":"结构化输出是让大模型生成符合特定格式的数据，而不是自由文本。\n其中，特定的格式可以是JSON、XML、表格等等。\n这样的输出可以被计算机程序直接解析和处理。\n常见的数据格式有：JSON、YAML、CSV、XML\n常见的结构化文本有：表格、键值对\n还有某些领域的特定格式，比如：SQL语句、HTML、LaTeX\n要实现这样的结构化输出，需要在输入提示中明确要求输出格式，并用代码解析模型生成的文本，转换为标准结构，然后使用库或者框架自动验证和解析输出格式。\n","date":"2026-01-09T13:00:00Z","permalink":"https://iamxurulin.github.io/p/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%9A%84%E7%BB%93%E6%9E%84%E5%8C%96%E8%BE%93%E5%87%BA%E6%98%AF%E4%BB%80%E4%B9%88/","title":"大模型的结构化输出是什么"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 ListNode merge(ListNode pHead1, ListNode pHead2) { // 任一链表为空，直接返回另一链表 if(pHead1==null){ return pHead2; } if(pHead2==null){ return pHead1; } // 虚拟节点：简化合并后链表头节点的处理 ListNode dummy = new ListNode(0); ListNode head = dummy; // 结果链表的当前指针，用于挂载节点 while(pHead1!=null\u0026amp;\u0026amp;pHead2!=null){ if(pHead1.val\u0026lt;=pHead2.val){ head.next = pHead1; pHead1=pHead1.next; }else{ head.next = pHead2; pHead2 = pHead2.next; } head=head.next; // 结果指针后移 } // 拼接剩余节点 if(pHead1!=null){ head.next = pHead1; }else{ head.next = pHead2; } // 返回合并后的链表 return dummy.next; } public ListNode sortInList (ListNode head) { // 空链表/单节点链表，直接返回 if(head ==null||head.next==null){ return head; } ListNode left = head; // 慢指针前驱（用于断开链表） ListNode mid = head.next; // 慢指针（最终指向中点） ListNode right = head.next.next; // 快指针（步长2） // 持续移动指针，直到快指针到末尾 while(right!=null\u0026amp;\u0026amp;right.next!=null){ left = left.next; // 慢指针前驱后移 mid = mid.next; // 慢指针后移（步长1） right = right.next.next; // 快指针后移（步长2） } left.next = null; // 断开链表，拆分为「head→left」和「mid→末尾」两段 // 递归排序两段链表，再合并结果 return merge(sortInList(head), sortInList(mid)); } ","date":"2026-01-08T23:31:15Z","permalink":"https://iamxurulin.github.io/p/%E5%BD%92%E5%B9%B6-%E5%8D%95%E9%93%BE%E8%A1%A8%E7%9A%84%E6%8E%92%E5%BA%8F/","title":"归并-单链表的排序"},{"content":" 代码求解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 public ListNode reverseList(ListNode pHead){ if(pHead == null){ return null; } ListNode pre = null; ListNode cur = pHead; ListNode next = pHead; while(cur!=null){ next = cur.next; cur.next = pre; pre = cur; cur = next; } return pre; } public ListNode addInList (ListNode head1, ListNode head2) { // 链表1为空，直接返回链表2 if (head1 == null) { return head2; } // 链表2为空，直接返回链表1 if (head2 == null) { return head1; } // 反转两个链表，让低位在前（方便从低位开始相加） head1 = reverseList(head1); head2 = reverseList(head2); ListNode dummy = new ListNode(-1); // 虚拟头节点：简化结果链表的头节点处理 ListNode head = dummy; // 结果链表的当前指针（用于挂载新节点） int carry = 0; // 进位标志 // head1未遍历完 || head2未遍历完 || 还有进位（包含carry!=0，处理最后一位相加的进位） while (head1 != null || head2 != null || carry != 0) { // 获取当前节点的值（链表已遍历完则取0，不影响相加结果） int val1 = head1 == null ? 0 : head1.val; int val2 = head2 == null ? 0 : head2.val; int temp = val1 + val2 + carry; carry = temp / 10; // 更新进位 temp %= 10; // 取当前位的结果 // 创建当前位的节点，挂载到结果链表上 head.next = new ListNode(temp); head = head.next; // 结果链表指针后移，准备挂载下一个节点 // 原链表指针后移 if (head1 != null) { head1 = head1.next; } if (head2 != null) { head2 = head2.next; } } // 反转结果链表，恢复高位在前的格式，返回最终结果 return reverseList(dummy.next); } ","date":"2026-01-08T21:52:51Z","permalink":"https://iamxurulin.github.io/p/%E9%93%BE%E8%A1%A8%E7%9B%B8%E5%8A%A0-%E4%BA%8C/","title":"链表相加-二"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { // 初始化两个临时指针，分别指向两个链表的头节点 ListNode temp1 = pHead1; ListNode temp2 = pHead2; // 只要两个指针不指向同一个节点，就继续遍历 while (temp1 != temp2) { // - temp1遍历完自己的链表（为null），就切换到链表2的头节点继续遍历 // - 否则，temp1正常后移 temp1 = temp1 == null ? pHead2 : temp1.next; // - temp2遍历完自己的链表（为null），就切换到链表1的头节点继续遍历 // - 否则，temp2正常后移 temp2 = temp2 == null ? pHead1 : temp2.next; } // temp1和temp2要么指向第一个公共节点，要么都为null（无公共节点） return temp1; } ","date":"2026-01-08T20:49:40Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%A4%E4%B8%AA%E9%93%BE%E8%A1%A8%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AA%E5%85%AC%E5%85%B1%E7%BB%93%E7%82%B9/","title":"两个链表的第一个公共结点"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public ListNode removeNthFromEnd (ListNode head, int n) { if (head == null || n \u0026lt;= 0) { return head; } // 虚拟头节点：解决删除头节点时无前驱的边界问题 ListNode dummy = new ListNode(-1); dummy.next = head; ListNode pre = dummy; ListNode fast = head; ListNode slow = head; // 快指针先单独移动n步 for (int i = 0; i \u0026lt; n; i++) { // 若fast已为null，说明n\u0026gt;链表长度，无节点可删，直接返回原链表 if (fast == null) { return dummy.next; } fast = fast.next; } // 快慢指针同步移动，直到快指针到链表末尾（fast=null） while (fast != null) { fast = fast.next; // 快指针先后移 pre = slow; // 把pre更新为当前的slow（此时slow还没动，pre\u0026#34;记住\u0026#34;了slow现在的位置） slow = slow.next; // slow再后移（此时pre已经是slow移动后的上一个节点） } // 执行删除操作：让前驱节点的next跳过slow，指向slow的下一个节点 pre.next = slow.next; // 返回新链表头 return dummy.next; } ","date":"2026-01-08T20:11:33Z","permalink":"https://iamxurulin.github.io/p/%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E7%9A%84%E5%80%92%E6%95%B0%E7%AC%ACn%E4%B8%AA%E8%8A%82%E7%82%B9/","title":"删除链表的倒数第n个节点"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public ListNode FindKthToTail (ListNode pHead, int k) { // 快慢指针开始都指向链表头节点 ListNode fast = pHead; ListNode slow = pHead; // 快指针先向前移动 k 步 for(int i=0;i\u0026lt;k;i++){ if(fast!=null){ fast=fast.next; }else{ // 若快指针提前到末尾（说明k \u0026gt; 链表长度），直接返回null return null; } } // 快慢指针同步向后移动，直到快指针指向null while(fast!=null){ fast=fast.next; slow=slow.next; } // 慢指针正好指向倒数第k个节点 return slow; } 为什么可以这么做呢？\n这里说明一下：\n假设啊，链表总长度是n，fast指针先移动k步后，剩下的没有走的长度就是n-k；\n之后，fast和slow指针同步移动，\n当fast指针走完剩下的n-k步到达末尾时，\nslow指针也恰好走了n-k步，\n而这个位置正好就是倒数第k个节点的位置。\n","date":"2026-01-08T19:30:40Z","permalink":"https://iamxurulin.github.io/p/%E9%93%BE%E8%A1%A8%E4%B8%AD%E5%80%92%E6%95%B0%E6%9C%80%E5%90%8Ek%E4%B8%AA%E7%BB%93%E7%82%B9/","title":"链表中倒数最后k个结点"},{"content":"PO（Persistent Object）持久化对象，主要用于和数据库交互，是数据库数据在内存中的镜像。\nVO（View Object）视图对象，和前端展示强相关，按需组装前端需要的字段。\nBO（Business Object）业务对象，封装业务逻辑，包含业务处理方法，是业务层专用，业务层只操作BO，不直接碰PO/DTO。\nDTO（Data Transfer Object）数据传输对象，屏蔽底层 PO 结构，可细分 ReqDTO（请求）和 ResDTO（响应），用于跨层或者跨服务传输数据。\nDAO（Data Access Object）数据访问对象，负责和数据库打交道，隔离业务逻辑和数据操作，依赖PO，通过操作PO完成与数据库的交互。\nPOJO（Plain Ordinary Java Object）简单Java对象，最基础的Java类。\nPO/VO/BO/DTO 本质上都是 POJO。\n常见的调用链路：\n前端请求 → Controller接收【ReqDTO】→ Service将ReqDTO转为【BO】→ BO调用【DAO】→ DAO操作【PO】与数据库交互 → Service将BO/PO转为【ResDTO/VO】→ Controller返回给前端\n","date":"2026-01-08T17:58:10Z","permalink":"https://iamxurulin.github.io/p/povobodtodaopojo%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB/","title":"PO、VO、BO、DTO、DAO、POJO有什么区别"},{"content":"为了确保消息不会丢失，可以从以下3个方面解决：\n1.在创建队列的时候设置durable为true，发布消息的时候设置delivery为2，从而确保队列和消息都是持久的。\n这样，就算是RabbitMQ服务器重启也不会造成消息的丢失。\n2.开启发布确认模式，这样的话，生产者会等待服务器的确认响应，确保消息已经成功存储。\n3.使用明确的消费者确认机制，当消费者处理完消息之后，向RabbitMQ发送确认，只有在RabbitMQ收到消费者发来的确认之后才会将消息从队列中删除。\n","date":"2026-01-08T17:15:44Z","permalink":"https://iamxurulin.github.io/p/%E5%9C%A8rabbitmq%E4%B8%AD-%E6%80%8E%E4%B9%88%E7%A1%AE%E4%BF%9D%E6%B6%88%E6%81%AF%E4%B8%8D%E4%BC%9A%E4%B8%A2%E5%A4%B1/","title":"在RabbitMQ中-怎么确保消息不会丢失"},{"content":"Java中的垃圾回收算法主要有3种，分别是标记-清除算法、复制算法、标记-整理算法。\n1.标记-清除算法\n这种算法的逻辑其实很简单，就是先遍历一遍，把有用的东西都打个勾✅（标记），然后把那些没打勾的垃圾直接扔掉（清除）。\n不过，这种算法存在一个缺点，就是会留下内存碎片。\n2.复制算法\n复制算法可以很好地解决内存碎片问题，这种算法是把内存一分为二，平时只用一半。\n回收的时候，会把活着的对象全部复制到另一半去，然后把原来的那一半直接清空。\n这种算法的优点是快，可以保证没有碎片，但是需轮流着一半的空间不能用，太浪费空间了。\n3.标记-整理算法\n标记-整理算法是老年代常用的算法。\n对老年代的对象，因为存活的时间长，如果采用复制算法，需要复制一大堆，速度太慢；\n但是，标记-清除又会产生碎片。\n所以，标记-整理算法是先进行标记，然后把所有活着的对象往一端推，再把剩下的空间全部清空。\n通过这种方式，既不会产生碎片，也不会浪费掉一半的空间，不过，把所有活着的对象往一端推这个整理的动作会比较耗时。\n","date":"2026-01-08T16:54:37Z","permalink":"https://iamxurulin.github.io/p/java%E4%B8%AD%E6%9C%89%E5%93%AA%E4%BA%9B%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95/","title":"Java中有哪些垃圾回收算法"},{"content":"Linux文件系统采用的是层次化的树形结构。\n/ 根目录，系统的起点，包括所有其他目录 /bin 存放系统必需的二进制可执行文件 /boot 存放引导加载程序和内核等系统启动相关的文件 /dev 存放像硬盘、终端设备、虚拟机等存储设备文件，这些文件只是接口，不存储实际数据 /etc 存放系统配置文件(用户账号)和脚本(启动脚本)等等 /home 普通用户的主目录，每个用户会有一个单独的子目录 /lib 必需的共享库和内核模块 /media 挂载点，用于挂载像U盘和光盘这类可移除介质 /mnt 一种临时挂载点，用于系统管理员手动挂载文件系统 /opt 用于安装附加软件包，通常是第三方应用程序 /proc 存放系统内核和进程信息的虚拟文件系统 /root root的主目录 /sbin 存放系统管理员使用的系统二进制文件 /tmp 临时文件目录，各种程序的临时存储空间 /usr 包含用户执行的应用程序和文件 /var 存放日志文件、邮件等这类经常变化的文件 ","date":"2026-01-08T16:25:43Z","permalink":"https://iamxurulin.github.io/p/%E6%95%B4%E7%90%86linux%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%E4%B8%AD%E5%90%84%E4%B8%AA%E7%9B%AE%E5%BD%95%E7%9A%84%E4%BD%9C%E7%94%A8/","title":"整理Linux文件系统中各个目录的作用"},{"content":"提示工程的设计技巧主要有：\n1.提示中需要清楚地说明AI的身份、能力边界和目标任务。\n2.需要用明确的格式指导AI输出。\n3.明确告知AI只能基于检索到的资料进行回答，以免出现“幻觉”。\n4.可以将提示设计成模板，预留动态的填充位，方便大规模复用和动态地填充检索内容。\n5.如果没有检索到相关的内容，不要使用模型自身的知识储备进行推断，直接让AI显示地回复“未找到相关的资料”。\n6.可以给1到2个优质的问答范例，让模型模仿输出风格和逻辑。\n","date":"2026-01-08T15:12:58Z","permalink":"https://iamxurulin.github.io/p/%E5%9C%A8rag%E5%BA%94%E7%94%A8%E4%B8%AD-%E6%9C%89%E5%93%AA%E4%BA%9B%E6%8F%90%E7%A4%BA%E5%B7%A5%E7%A8%8B%E8%AE%BE%E8%AE%A1%E6%8A%80%E5%B7%A7/","title":"在RAG应用中-有哪些提示工程设计技巧"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public ListNode EntryNodeOfLoop(ListNode pHead) { // 快慢指针都从链表头出发 ListNode fast = pHead; ListNode slow = pHead; while (fast != null \u0026amp;\u0026amp; fast.next != null) { fast = fast.next.next; // 快指针走2步 slow = slow.next; // 慢指针走1步 // 快慢指针相遇 → 证明链表有环，进入找入口逻辑 if (slow == fast) { // 找环的入口 while (pHead != slow) { pHead = pHead.next; // 头指针走1步 slow = slow.next; // 相遇点指针走1步 } return slow; // 相遇时就是环的入口 } } // 无环，返回null return null; } ","date":"2026-01-07T23:07:19Z","permalink":"https://iamxurulin.github.io/p/%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%8E%AF%E7%9A%84%E5%85%A5%E5%8F%A3%E7%BB%93%E7%82%B9/","title":"链表中环的入口结点"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public boolean hasCycle(ListNode head) { // 空链表直接无环 if (head == null) { return false; } // 快慢指针都从表头出发 ListNode slow = head; ListNode fast = head; while (fast != null \u0026amp;\u0026amp; fast.next != null) { slow = slow.next; // 慢指针走1步 fast = fast.next.next; // 快指针走2步 // 快慢指针相遇 → 链表有环 if (fast == slow) { return true; } } //快指针走到尾部，无环 return false; } 这里说明一下循环条件为什么要这么写：\n因为快指针的核心操作是：\n1 fast=fast.next.next 这一步其实可以等价拆解为两步的连续next调用：\n1 2 3 ListNode temp = fast.next;//取fast的下一个节点 fast = temp.next;//取temp的下一个节点 要让这两步都不触发空指针异常，需要满足以下两个前提：\nfast.next不报错，也就是fast不能为null temp.next不报错，也就是temp不能为null，即fast.next不能为null 因此，循环条件就是：\n1 fast!=null\u0026amp;\u0026amp;fast.next!=null ","date":"2026-01-07T21:57:22Z","permalink":"https://iamxurulin.github.io/p/%E5%88%A4%E6%96%AD%E9%93%BE%E8%A1%A8%E4%B8%AD%E6%98%AF%E5%90%A6%E6%9C%89%E7%8E%AF/","title":"判断链表中是否有环"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public static ListNode mergeKLists(ArrayList\u0026lt;ListNode\u0026gt; arr) { // 初始化小顶堆：按节点值升序排序，每次取出当前最小的节点 PriorityQueue\u0026lt;ListNode\u0026gt; pq = new PriorityQueue\u0026lt;ListNode\u0026gt;((a, b) -\u0026gt; (a.val - b.val)); // 将非空的头节点加入小顶堆 for (int i = 0; i \u0026lt; arr.size(); i++) { ListNode head = arr.get(i); if (head != null) { // 跳过空链表，避免堆中加入null pq.add(head); } } // 哨兵节点：简化链表拼接逻辑，无需处理头节点为空的情况 ListNode dummy = new ListNode(-1); ListNode curr = dummy; // 合并链表的尾节点指针，初始指向哨兵节点 while (!pq.isEmpty()) { ListNode minNode = pq.poll(); // 取出当前所有链表的最小节点 curr.next = minNode; // 拼接到合并链表尾部 curr = curr.next; // 尾节点指针后移 // 若当前最小节点的下一个节点非空，加入堆中继续参与排序 if (minNode.next != null) { pq.add(minNode.next); } } // 哨兵节点的next即为合并后的真实头节点 return dummy.next; } ","date":"2026-01-07T21:30:37Z","permalink":"https://iamxurulin.github.io/p/%E5%90%88%E5%B9%B6k%E4%B8%AA%E5%B7%B2%E6%8E%92%E5%BA%8F%E7%9A%84%E9%93%BE%E8%A1%A8/","title":"合并k个已排序的链表"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public ListNode Merge(ListNode pHead1, ListNode pHead2) { // 任一链表为空，直接返回另一个链表 if (pHead1 == null || pHead2 == null) { return pHead1 == null ? pHead2 : pHead1; } // 选择两个链表头中值更小的作为合并后的头节点 ListNode mergeHead = pHead1.val \u0026lt; pHead2.val ? pHead1 : pHead2; // currSelected：选中头节点的链表的下一个节点 ListNode currSelected = mergeHead.next; // currOther：未选中头节点的另一个链表的头节点 ListNode currOther = mergeHead == pHead1 ? pHead2 : pHead1; // pre：合并链表的尾节点 ListNode pre = mergeHead; while (currSelected != null \u0026amp;\u0026amp; currOther != null) { // 选择值更小的节点拼接到合并链表尾部 if (currSelected.val \u0026lt;= currOther.val) { pre.next = currSelected; currSelected = currSelected.next; // 选中链表指针后移 } else { pre.next = currOther; currOther = currOther.next; // 另一个链表指针后移 } pre = pre.next; // 合并链表尾节点后移 } // 拼接剩余未遍历完的链表 pre.next = currSelected == null ? currOther : currSelected; return mergeHead; } ","date":"2026-01-07T19:19:47Z","permalink":"https://iamxurulin.github.io/p/%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%8E%92%E5%BA%8F%E7%9A%84%E9%93%BE%E8%A1%A8/","title":"合并两个排序的链表"},{"content":"\n代码求解 要实现每k个一组翻转链表，我们先来手撕一下反转整个链表：\n1 2 3 4 5 6 7 8 9 10 11 12 13 //反转以a为头节点的链表 ListNode reverse(ListNode a){ ListNode pre = null; ListNode cur = a; ListNode next = a; while(cur!=null){ next = cur.next; cur.next = pre; pre = cur; cur = next; } return pre; } 我们再来手撕一下反转a到b之间的节点：\n注意是左闭右开区间。\n1 2 3 4 5 6 7 8 9 10 11 12 13 //反转区间[a,b)的节点 ListNode reverse(ListNode a,ListNode b){ ListNode pre = null; ListNode cur = a; ListNode next = a; while(cur!=b){ next = cur.next; cur.next = pre; pre = cur; cur = next; } return pre; } 所以，实现每k个一组反转就是：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 ListNode reverseKGroup(ListNode head,int k){ if(head == null){ return null; } ListNode a = head; ListNode b = head; for(int i=0;i\u0026lt;k;i++){ if(b==null){ return head; } b=b.next; } //反转[a,b)区间的链表，得到新的头节点 ListNode newHead = reverse(a,b); //递归处理剩余区间，拼接链表 a.next = reverseKGroup(b,k); return newHead; } ListNode reverse(ListNode a,ListNode b){ ListNode pre = null; ListNode cur = a; ListNode next = a; while(cur!=b){ next = cur.next; cur.next = pre; pre = cur; cur = next; } return pre; } ","date":"2026-01-07T17:40:08Z","permalink":"https://iamxurulin.github.io/p/%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9%E6%AF%8Fk%E4%B8%AA%E4%B8%80%E7%BB%84%E7%BF%BB%E8%BD%AC/","title":"链表中的节点每k个一组翻转"},{"content":"JVM主要由类加载子系统（ClassLoader）、运行时数据区（Runtime Data Area）、执行引擎（Execution Engine）以及本地方法接口（Native Interface）4个部分组成。\nJVM就像一个虚拟的“电脑”，能让Java程序在不同的操作系统上跑起来。\n核心工作流程是：\n编写好的Java代码在编译成class文件之后，\n类加载器首先负责把class文件从磁盘或者网络中拉进来，放入到内存中；\n运行时数据区主要用于存放代码和变量；\n执行引擎像个翻译官，把Java的字节码转成机器码；\n当需要调用C++之类的外部代码时，就通过本地方法接口来帮忙桥接。\n","date":"2026-01-07T16:43:37Z","permalink":"https://iamxurulin.github.io/p/jvm%E7%94%B1%E5%93%AA%E4%BA%9B%E9%83%A8%E5%88%86%E7%BB%84%E6%88%90/","title":"JVM由哪些部分组成"},{"content":"Spring Boot支持以下4种嵌入Web容器：\n1.Tomcat\nSpring Boot默认的嵌入式Web容器，Spring Boot会自动地将Tomcat内嵌到应用程序中，是一种广泛使用的轻量级、广泛使用的Servlet容器。\n2.Jetty\nJetty比Tomcat更轻量，是一个高效的Web服务器和Servlet容器，通常用于嵌入式系统或对资源占用较为敏感的环境。\n3.undertow\nundertow支持异步IO和HTTP/2，是一个轻量级的高性能Web服务器和Servlet容器，适用于处理高并发的HTTP请求。\n4.Netty\nNetty是一个非阻塞的异步事件驱动框架，是Spring WebFlux的默认嵌入式容器，仅适用于响应式编程模型，适合高并发的响应式应用。\n","date":"2026-01-07T16:27:41Z","permalink":"https://iamxurulin.github.io/p/spring-boot%E6%94%AF%E6%8C%81%E5%93%AA%E4%BA%9B%E5%B5%8C%E5%85%A5web%E5%AE%B9%E5%99%A8/","title":"Spring-Boot支持哪些嵌入Web容器"},{"content":"优先级如下：\n命令行参数\u0026gt; JAR包外 的application-{profile}.properties\u0026gt; JAR 包外的application.properties\u0026gt;JAR包内的application-{profile}.properties\u0026gt;JAR包内的application.properties。\n需要注意的是：\n当application.properties和application.yml同时存在时，如果是相同的参数，最终生效的是application.properties中的配置。\n","date":"2026-01-07T15:59:34Z","permalink":"https://iamxurulin.github.io/p/%E4%BD%A0%E7%9F%A5%E9%81%93spring-boot%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E7%9A%84%E5%8A%A0%E8%BD%BD%E4%BC%98%E5%85%88%E7%BA%A7%E5%90%97/","title":"你知道Spring-Boot配置文件的加载优先级吗"},{"content":"在Linux系统中进行磁盘配额管理的核心步骤：\n1.安装磁盘配额管理工具quota\n如果是Ubuntu/Debian系列的Linux系统，输入以下命令：\n1 sudo apt-get install quota 如果是CentOS/RHEL系列的Linux系统，输入以下命令：\n1 sudo yum install quota 2.编辑/etc/fstab文件，为需要启用配额的分区添加usrquota和grpquota选项。\n1 /dev/sda1 /home ext4 defaults,usrquota,grpquota 0 2 3.为了使第2步的配置生效，需要重新挂载分区\n1 sudo mount -o remount /home 4.生成配额文件aquota.user（用户配额文件）和aquota.group（组配额文件）并初始化配额数据库。\n1 2 3 4 #c=创建配额文件，u=用户，g=组，m=强制检查，v=显示详细过程 sudo quotacheck -cugmv /home sudo quotaon /home 5.使用edquota命令为用户或组设置软性和硬性限制。\n1 sudo edquota username ","date":"2026-01-07T15:49:05Z","permalink":"https://iamxurulin.github.io/p/%E5%9C%A8linux%E7%B3%BB%E7%BB%9F%E4%B8%AD-%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%E7%A3%81%E7%9B%98%E9%85%8D%E9%A2%9D%E7%AE%A1%E7%90%86/","title":"在Linux系统中-如何进行磁盘配额管理"},{"content":"在RAG中选择Embedding Model时，主要考虑的是以下7大因素：\n1.模型能否精准地捕捉文本语义，准确性直接影响到向量相似度计算的可靠性。（语义准确性）\n2.模型的推理速度能否满足业务的实时性要求，显存和内存的占用能否适配当下的硬件资源。（模型的效率）\n3.是否专门针对某个垂直领域做过预训练或者微调，原生支持特定术语和逻辑。（领域适配）\n4.是否支持业务所需的语言，以及具备跨语言对齐能力。（多语言支持）\n5.模型的参数量和训练数据规模是否匹配语料的复杂度。（数据规模匹配）\n6.是否开源、是否有活跃的社区在维护，API的调用是否灵活。（开放性与生态）\n7.训练和推理的硬件投入成本以及使用成本。（成本）\n","date":"2026-01-07T14:43:53Z","permalink":"https://iamxurulin.github.io/p/%E5%9C%A8rag%E4%B8%AD%E9%80%89%E6%8B%A9embedding-model%E9%9C%80%E8%A6%81%E8%80%83%E8%99%91%E5%93%AA%E4%BA%9B%E5%9B%A0%E7%B4%A0/","title":"在RAG中选择Embedding-Model需要考虑哪些因素"},{"content":"\n代码求解 reverseN函数是实现将链表的前n个节点进行反转。\n和反转整个链表不同，reverseN是要让反转之后的head节点和后面的节点连接起来，而不是指向null。\n因此，head.next需要指向successor。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ListNode successor = null; public ListNode reverseBetween (ListNode head, int m, int n) { if(m==1){ return reverseN(head,n); } head.next=reverseBetween(head.next,m-1,n-1); return head; } public ListNode reverseN(ListNode head,int n){ if(n==1){ successor = head.next; return head; } ListNode last = reverseN(head.next,n-1); head.next.next = head; head.next = successor; return last; } ","date":"2026-01-06T22:40:21Z","permalink":"https://iamxurulin.github.io/p/%E9%93%BE%E8%A1%A8%E5%86%85%E6%8C%87%E5%AE%9A%E5%8C%BA%E9%97%B4%E5%8F%8D%E8%BD%AC/","title":"链表内指定区间反转"},{"content":"RabbitMQ是一个可实现异步通信和任务解耦的消息队列系统。\n主要有Direct、Fanout、Topic、Headers这4种类型。\nDirect 根据消息的routing key精确匹配binding key，只有完全匹配的消息才会被转发到对应的Queue 适合像日志系统这种需要精确匹配的场景 Fanout 不考虑routing key，直接将接收到的每一条消息都广播到所有绑定到它的Queue 适合广播消息，比如社交媒体的消息推送 Topic 根据消息的routing key和binding key的模式匹配决定消息的流转路径 适合需要根据特定模式进行消息路由的场景，比如订单系统 Headers 根据消息的头部属性来路由，提供更加复杂和灵活的路由策略 适合需要更复杂路由逻辑的场景，比如不同部门处理的邮件系统 ","date":"2026-01-06T17:52:56Z","permalink":"https://iamxurulin.github.io/p/rabbitmq%E7%9A%84%E4%BA%A4%E6%8D%A2%E6%9C%BA%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D%E7%B1%BB%E5%9E%8B/","title":"RabbitMQ的交换机有哪几种类型"},{"content":"在操作系统层面，一个完整的生命周期中包含5种基础状态，分别是新建（New）、就绪（Ready）、运行（Running）、阻塞（Blocked/Waiting）、终止（Terminated）。这5种状态中，中间的就绪、运行、阻塞又是核心状态。\n我们可以把进程的这5种状态的转换想象成去银行的柜台办理业务的流程：\n新建状态下，操作系统正在为进程分配进程ID（PID）和进程控制块（PCB），相当于我去银行取好了号，正在填写个人信息，但此时并没有进入到等待大厅；\n就绪状态就是只要CPU有空闲，进程就能马上执行，相当于我的材料这些都准备齐了，坐在等待大厅里等着叫号；\n运行状态下，进程此时占用了CPU的资源正在执行指令，相当于叫到我了，我此时就在柜台办理业务；\n阻塞状态就是进程执行到一半，需要等待某个I/O操作或者某个文件的输入完成后才能往下执行，此时就进入了阻塞状态，相当于业务办理到一半，发现我的身份证复印件没带，让我选择去附近打印室复印或者通知家人朋友送来，这个时候只能等复印件拿到手之后，才能重新排队继续办理，这时不能插队；\n终止状态，进程执行完毕或者发生了异常退出，资源被回收，相当于业务办理完了或者和柜员拌嘴被保安赶走，柜员可以给其他人接着办理了。\n","date":"2026-01-06T17:32:22Z","permalink":"https://iamxurulin.github.io/p/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9A%84%E8%BF%9B%E7%A8%8B%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D%E7%8A%B6%E6%80%81/","title":"操作系统的进程有哪几种状态"},{"content":"平常的办公中，我们用的比较多的是Windows系统，应该也遇到过想删除一个文件但是显示这个文件被某个应用程序占用的问题吧，这个时候我们可以根据提示打开任务管理器把这个程序关闭，就能够删除这个文件了。\n那在Linux系统的命令行界面怎么排查文件的占用问题呢？\n可以使用lsof命令。\n这个命令可以列出当前打开的文件以及关联的进程。\n1.如果需要查找某个特定文件被哪个进程占用，可以执行：\n1 lsof /path/to/username/file 以上命令可以列出所有打开该文件的进程信息，像进程ID、用户、文件描述符等等。\n2.如果需要查找某个特定端口被哪个进程占用，可以执行：\n1 lsof -i :portnumber 3.如果想要查看某个特定用户打开了哪些文件，可以执行：\n1 lsof -u username 4.当你找到了占用文件的进程ID(PID)之后，可以使用kill命令来终止这个进程：\n1 kill -9 PID ","date":"2026-01-06T16:52:26Z","permalink":"https://iamxurulin.github.io/p/%E5%9C%A8linux%E7%B3%BB%E7%BB%9F%E4%B8%AD%E6%80%8E%E4%B9%88%E6%8E%92%E6%9F%A5%E6%96%87%E4%BB%B6%E5%8D%A0%E7%94%A8%E9%97%AE%E9%A2%98/","title":"在Linux系统中怎么排查文件占用问题"},{"content":"使用top命令可以获取到CPU使用率、内存使用情况、运行中的进程等重要信息。\n步骤：\n1.在终端输入top命令，按Enter键\n2.之后就能看到一个实时更新的数据表，该数据表主要包含以下信息：\n系统信息 当前时间、系统运行时间、用户数和平均负载 进程摘要 总进程数、运行的进程数、睡眠的进程数 CPU状态 CPU的不同使用状态，比如：用户空间(us)、系统空间(sy)、空闲空间(id) 内存和交换空间 物理内存和交换内存的使用情况 进程列表 当前所有运行进程的信息，包括进程ID(PID)、用户、优先级、CPU使用率、内存使用率等 ","date":"2026-01-06T16:26:35Z","permalink":"https://iamxurulin.github.io/p/%E4%BD%BF%E7%94%A8linux%E7%9A%84top%E5%91%BD%E4%BB%A4%E8%BF%9B%E8%A1%8C%E6%80%A7%E8%83%BD%E7%9B%91%E6%8E%A7%E7%9A%84%E6%AD%A5%E9%AA%A4/","title":"使用Linux的top命令进行性能监控的步骤"},{"content":"把文本内容、图像、音频、视频等形式的信息映射为高维空间中的密集向量的过程就是“嵌入”。\n向量是语义空间中的坐标，主要用于捕捉对象之间的语义关系和隐含的意义。\n每个向量相当于文本的数字指纹，里面包含了文本的语义信息。\n一般来说，语义相近的对象在向量空间中彼此接近，语义相异的对象则彼此远离。\n在向量空间中进行数学计算可以判断两段话是否相关。\n分块后的文本块需要先生成Embedding，存入到向量数据库中，在用户提问时，系统通过计算提问的Embedding 与文本块的Embeding之间的相似度，找到和用户的提问最相关的内容，再交给大模型生成回答。\n那为什么需要Embedding呢？\n主要是因为传统的检索比较依赖关键词匹配，难以应对同义词、上下文和多样化表达的问题。\nEmbedding是将文本映射到高维的向量空间，如果用户问“怎么泡咖啡”，经过Embedding之后可以将“咖啡的制作步骤”等语义相关的概念通过向量距离自动匹配。\n","date":"2026-01-06T15:43:40Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4rag%E4%B8%AD%E7%9A%84embedding%E5%B5%8C%E5%85%A5/","title":"说说RAG中的Embedding嵌入"},{"content":" -Xms 初始化堆内存大小 -Xmx 最大堆内存大小 -Xss 设置每个线程的栈大小 -XX:MetaspaceSize 初始化元空间大小 -XX:MaxMetaspaceSize 最大元空间大小 -XX:+HeapDumpOnOutOfMemoryError 当发生OOM时，生成堆转储 -XX:+PrintGCDetails 打印详细的垃圾回收日志 -XX:+UseG1GC 启用G1垃圾收集器 -XX:+UseConcMarkSweepGC 启用CMS垃圾收集器 -XX:+UseZGC 启用ZGC低延迟垃圾收集器 ","date":"2026-01-05T15:48:48Z","permalink":"https://iamxurulin.github.io/p/%E5%B8%B8%E7%94%A8%E7%9A%84jvm%E9%85%8D%E7%BD%AE%E5%8F%82%E6%95%B0/","title":"常用的JVM配置参数"},{"content":"RabbitMQ的架构有以下6个关键组件：\nProducer 生产者 主要负责生产消息并发送给RabbitMQ Exchange 交换机 生产者发过来的消息并不是直接投进队列，而是 交给Exchange，让Exchange根据消息中的Routing Key来决定把它扔进哪个队列 Queue 队列 这是消息最终存放的地方，在这里等待被取走 Binding 绑定 把Exchange和Queue连接起来，告诉交换机这个消息需要扔进哪个队列 Consumer 消费者 从Queue中取出消息并处理 Virtual Host 虚拟主机 VHost之间是完全隔离的，互不影响，通常用于区分不同的业务线 如果把RabbitMQ比作快递物流系统，那么6个关键组件分别对应的角色如下：\n生产者是发件人，交换机是快递分拣中心，队列就是快递仓库，绑定就是分拣规则表，消费者就是收件人，虚拟主机就是快递分公司。\n注意⚠️：\n一个消息可以被拷贝到多个Queue，但是一个队列里的消息只能被消费一次。\n","date":"2026-01-05T15:37:48Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4rabbitmq%E7%9A%84%E5%9F%BA%E6%9C%AC%E6%9E%B6%E6%9E%84/","title":"说说RabbitMQ的基本架构"},{"content":"六种。\nsingleton 默认是单例，一个 Spring IOC容器的内部只有一个Bean实例 prototype 原型，每次获取都会新建实例 request 每个请求都会新建一个属于自己的Bean实例 session 一个http session中有一个Bean的实例 application 整个ServletContext生命周期里，只有一个Bean实例 websocket 一个WebSocket生命周期内只有一个Bean实例 request、session、application、websocket这四种作用域都是只存在于Spring Web应用中的。\n","date":"2026-01-05T14:53:26Z","permalink":"https://iamxurulin.github.io/p/spring-bean%E4%B8%80%E5%85%B1%E6%9C%89%E5%87%A0%E7%A7%8D%E4%BD%9C%E7%94%A8%E5%9F%9F/","title":"Spring-Bean一共有几种作用域"},{"content":"在RAG（检索增强生成）中，提示压缩主要是对检索出的文档内容通过提取核心信息、过滤无关文本、压缩冗长内容的方式进行精简处理，使得最终输入给大模型的内容既能够保留关键的信息，又符合模型输入长度的限制。\n那为什么要这么做呢？\n因为RAG的生成效果十分依赖“输入给模型的文档质量”。\n1.如果直接将大量拼接的检索内容输入给大模型，很可能会超出大模型的输入长度限制，而通过压缩可以将关键的信息浓缩进有限的token内。\n2.检索出的文档中很有可能包含大量的无关内容，这些内容往往会稀释关键信息，最终导致大模型聚焦困难，抓不住重点，如果不经过压缩生成的回答可能出现偏差，甚至出现“幻觉”。\n3.如果输入的文档存在较多的非必要内容，不经过压缩将增加模型处理和推理的计算负担，像商业的大模型都是通过token来计费的，因此还可能增加成本。\n","date":"2026-01-05T14:11:44Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%BA%E4%BB%80%E4%B9%88%E5%9C%A8rag%E4%B8%AD%E9%9C%80%E8%A6%81%E6%8F%90%E7%A4%BA%E5%8E%8B%E7%BC%A9/","title":"为什么在RAG中需要提示压缩"},{"content":"在AI生态系统中，A2A协议和MCP协议是两个扮演着不同角色但是又互补的标准协议。\nA2A（Agent to Agent）协议是一个应用层协议，允许不同的AI智能体以“智能体”或者“用户”的身份进行交流，更关注智能体之间的沟通和协作。\nMCP（Model Context Protocol）协议则提供了AI模型与外部的数据源和工具的标准化连接方式，允许AI模型通过统一的接口访问文件、数据库、API等资源。\nA2A类似于一个电话簿，目的是让不同的AI智能体可以相互 联系和协作；\n而MCP则类似于一个工具说明书，主要是为了告诉AI模型怎么使用外部的工具和数据。\n","date":"2026-01-05T13:34:53Z","permalink":"https://iamxurulin.github.io/p/a2a%E5%8D%8F%E8%AE%AE%E5%92%8Cmcp%E5%8D%8F%E8%AE%AE%E7%9A%84%E5%85%B3%E7%B3%BB/","title":"A2A协议和MCP协议的关系"},{"content":" 求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 public static int MAXN = 100001; public static int[] father = new int[MAXN]; public static boolean[] secret = new boolean[MAXN]; public static void build(int n,int first){ for(int i=0;i\u0026lt;n;i++){ father[i]=i; secret[i]=false; } father[first]=0; secret[0]=true; } public static int find(int i){ if(i!=father[i]){ father[i]=find(father[i]); } return father[i]; } public static void union(int x,int y){ int fx = find(x); int fy = find(y); if(fx!=fy){ father[fx]=fy; secret[fy]|=secret[fx]; } } public static List\u0026lt;Integer\u0026gt; findAllPeople(int n,int[][] meetings,int first){ build(n, first); Arrays.sort(meetings,(a,b)-\u0026gt;a[2]-b[2]); int m = meetings.length; for(int l=0,r;l\u0026lt;m;){ r=l; while (r+1\u0026lt;m\u0026amp;\u0026amp;meetings[l][2]==meetings[r+1][2]) { r++; } for(int i=l;i\u0026lt;=r;i++){ union(meetings[i][0], meetings[i][1]); } for(int i=l,a,b;i\u0026lt;=r;i++){ a=meetings[i][0]; b=meetings[i][1]; if(!secret[find(a)]){ father[a]=a; } if(!secret[find(b)]){ father[b]=b; } } l=r+1; } List\u0026lt;Integer\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); for(int i=0;i\u0026lt;n;i++){ if(secret[find(i)]){ ans.add(i); } } return ans; } ","date":"2026-01-04T19:53:26Z","permalink":"https://iamxurulin.github.io/p/leetcode2092%E6%89%BE%E5%87%BA%E7%9F%A5%E6%99%93%E7%A7%98%E5%AF%86%E7%9A%84%E6%89%80%E6%9C%89%E4%B8%93%E5%AE%B6/","title":"Leetcode2092找出知晓秘密的所有专家"},{"content":" jmap 一种用于生成堆转储的命令行工具，可以用于分析JVM的内存使用情况 jstack 一种用于生成线程转储的命令行工具，可以用于分析线程的状态 jstat 一种用于监控JVM统计信息的命令行工具，可提供实时的性能数据 MAT 一种用于分析堆转储文件的工具，可以帮助识别内存泄漏和优化内存使用 jconsole 可以监控JVM的内存使用，垃圾回收、线程、类加载等信息 VisualVM 可实时显示JVM的内存使用、垃圾回收类加载等信息 Arthas 一个强大的Java诊断工具，提供实时监控和分析的功能 ","date":"2026-01-04T16:32:39Z","permalink":"https://iamxurulin.github.io/p/%E6%95%B4%E7%90%86%E4%B8%80%E4%BA%9B%E5%8F%AF%E7%94%A8%E6%9D%A5%E5%88%86%E6%9E%90jvm%E6%80%A7%E8%83%BD%E7%9A%84%E5%B7%A5%E5%85%B7/","title":"整理一些可用来分析JVM性能的工具"},{"content":"RabbitMQ是一个开源的消息中间件。\n如果没有RabbitMQ，系统A直接把数据传给系统B，这样当B忙不过来或者挂了的时候，A就会卡死或者造成数据丢失。\n但是，如果有了RabbitMQ，系统A可以把数据传给RabbitMQ，而RabbitMQ则负责把数据暂存起来，再按照系统B的处理能力，稳稳地传送给B。\nRabbitMQ主要有3大应用场景：\n1.比如在实现用户注册功能时，通过引入RabbitMQ进行异步处理，可以提升响应的速度：\n没有引入RabbitMQ时，在注册完之后，需要同步地发邮件和短信，这样用户可能需要等几秒钟才能看到“注册成功”。\n引入RabbitMQ之后，把注册的信息写入数据库之后，可以直接把“发送邮件”和“发送短信”这两个任务扔给RabbitMQ，这样就能够立刻给用户返回“注册成功”。\n2.我们平常网购的时候，应该也会注意到，买东西的次数多了，某宝或者某东、某多的积分也会相应增加。\n这也就是下单场景的积分累加。\n想象一下如果订单系统直接调用积分接口，一旦积分系统挂了，那订单也下不了了。\n有个大佬说过这样一句话：\n没有什么是加一层中间层不能解决的，如果有，那就再加一层。\n所以，中间加个RabbitMQ，这样订单系统只需要把“我要下单”的消息发送给RabbitMQ，而积分系统什么时候恢复那就什么时候再去取消息，从而累加积分。\n通过这样引入RabbitMQ，可以实现应用之间的解耦，从而降低系统的依赖。\n3.在双十一的秒杀场景下，通常会有几百上千万的请求在一瞬间涌进来，这个时候数据库可能就直接崩了。\n通过引入RabbitMQ，先把请求全部扔进RabbitMQ排队，后台系统再按照自己的节奏从RabbitMQ中拉取请求进行处理，从而实现流量削峰，保护数据库的作用。\n","date":"2026-01-04T16:07:46Z","permalink":"https://iamxurulin.github.io/p/%E8%81%8A%E8%81%8Arabbitmq/","title":"聊聊RabbitMQ"},{"content":"在计算机程序中，当不再被使用的内存没有被程序释放，随着时间的推移，这些内存会逐渐累积起来，最后导致系统内存不足，这就是所谓的内存泄漏。\n内存泄漏轻则导致程序性能下降，重则导致系统崩溃。\n那么排查内存泄漏则有以下方法：\n1.使用top或者htop命令监视系统的整体内存使用情况，来帮助识别具体哪个进程占用了过多的内存。\n2.使用free命令查看系统内存的使用情况，包括总内存、已用内存、空闲内存、缓存内存。\n3.使用ps命令列出进程的详细信息，包括内存的使用情况。\n","date":"2026-01-04T15:30:40Z","permalink":"https://iamxurulin.github.io/p/%E4%BD%A0%E7%BB%99%E6%88%91%E8%A7%A3%E9%87%8A%E4%B8%80%E4%B8%8Blinux%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E9%97%AE%E9%A2%98%E5%92%8C%E6%8E%92%E6%9F%A5%E7%9A%84%E6%96%B9%E6%B3%95/","title":"你给我解释一下Linux系统中的内存泄露问题和排查的方法"},{"content":"查询拓展是指对用户的原始查询通过添加同义词、相关术语、隐含意图等信息的方式进行优化和补充，使得查询更加精准、覆盖范围更广，从而提升信息检索的效果。\n接下来说说为什么在RAG中需要进行查询扩展？\n如果原始查询不够准确或者覆盖范围不足，就会导致检索到的文档不相关或者信息不全，最终导致生成的回答质量会受到影响。\n如果增加了查询扩展，当用户的用词和知识库中的术语不一致时，可以在扩展后匹配更多的相关内容；\n还有就是用户的查询中的描述可能会比较简短或者模糊不清，这样在扩展后能够更加明确需求，使得检索更加精准。\n","date":"2026-01-04T15:03:51Z","permalink":"https://iamxurulin.github.io/p/%E8%A7%A3%E9%87%8A%E4%B8%80%E4%B8%8B%E4%BB%80%E4%B9%88%E6%98%AF%E6%9F%A5%E8%AF%A2%E6%8B%93%E5%B1%95/","title":"解释一下什么是查询拓展"},{"content":"Spring Boot Actuator是一个具有监控和管理功能的工具。\n通过Actuator可以监控应用的健康状况、查看指标、跟踪请求、管理环境参数等等。\nActuator的优势：\n1.开箱即用，可以很容易地与外部监控系统集成。\n2.提供了大量预定义的端点，可以用于展示和操作应用的内部状态，并且还支持自定义端点，扩展现有的端点功能。\n3.提供了关于应用、系统、JVM、内存、线程等丰富的信息。\n4.支持端点的安全访问控制，可以限制哪些端点可以公开访问。\n","date":"2026-01-04T14:33:48Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4spring-boot-actuator/","title":"说说Spring-Boot-Actuator"},{"content":"\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public static HashMap\u0026lt;Integer,Integer\u0026gt; rowFirst = new HashMap\u0026lt;Integer,Integer\u0026gt;(); public static HashMap\u0026lt;Integer,Integer\u0026gt; colFirst = new HashMap\u0026lt;Integer,Integer\u0026gt;(); public static int MAXN = 1001; public static int[] father = new int[MAXN]; public static int sets; public static void build(int n){ rowFirst.clear(); colFirst.clear(); for(int i=0;i\u0026lt;n;i++){ father[i]=i; } sets = n; } public static int find(int i){ if (i!=father[i]) { father[i]=find(father[i]); } return father[i]; } public static void union(int x,int y){ int fx = find(x); int fy = find(y); if (fx!=fy) { father[fx]=fy; sets--; } } public static int removeStones(int[][] stones){ int n = stones.length; build(n); for(int i =0;i\u0026lt;n;i++){ int row = stones[i][0]; int col = stones[i][1]; if(!rowFirst.containsKey(row)){ rowFirst.put(row, i); }else{ union(i, rowFirst.get(row)); } if(!colFirst.containsKey(col)){ colFirst.put(col, i); }else{ union(i, colFirst.get(col)); } } return n-sets; } ","date":"2026-01-03T23:05:05Z","permalink":"https://iamxurulin.github.io/p/%E5%B9%B6%E6%9F%A5%E9%9B%86-leetcode947%E7%A7%BB%E9%99%A4%E6%9C%80%E5%A4%9A%E7%9A%84%E5%90%8C%E8%A1%8C%E6%88%96%E5%90%8C%E5%88%97%E7%9F%B3%E5%A4%B4/","title":"并查集-Leetcode947移除最多的同行或同列石头"},{"content":"\n题目分析 要区分二维矩阵中的每个1，需要将二维问题转化为一维编号问题。\n如果矩阵有m列，位于第i行、第j列的元素对应的一维编号为i乘以m加j。\n先将所有的1建成小集合，遍历矩阵，遇到1时，如果其左边或者上边有1则进行合并，最后统计并查集中集合的数量就是岛的数量。\n代码求解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public static int numIslands(char[][] board){ int n = board.length; int m = board[0].length; build(n,m,board); for(int i=0;i\u0026lt;n;i++){ for(int j=0;j\u0026lt;m;j++){ if(board[i][j]==\u0026#39;1\u0026#39;){ if(j\u0026gt;0\u0026amp;\u0026amp;board[i][j-1]==\u0026#39;1\u0026#39;){ union(i,j,i,j-1); } if(i\u0026gt;0\u0026amp;\u0026amp;board[i-1][j]==\u0026#39;1\u0026#39;){ union(i,j,i-1,j); } } } } return sets; } public static int MAXSIZE = 100001; public static int[] father = new int[MAXSIZE]; public static int cols; public static int sets; public static void build(int n,int m,char[][] board){ cols = m; sets = 0; for(int a=0;a\u0026lt;n;a++){ for(int b=0,index;b\u0026lt;m;b++){ if(board[a][b]==\u0026#39;1\u0026#39;){ index = index(a,b); father[index]=index; sets++; } } } } public static int index(int a,int b){ return a*cols+b; } public static int find(int i){ if(i!=father[i]){ father[i]=find(father[i]); } return father[i]; } public static void union(int a,int b,int c,int d){ int fx = find(index(a, b)); int fy = find(index(c, d)); if(fx!=fy){ father[fx]=fy; sets--; } } ","date":"2026-01-03T22:25:01Z","permalink":"https://iamxurulin.github.io/p/leetcode200%E5%B2%9B%E5%B1%BF%E6%95%B0%E9%87%8F/","title":"Leetcode200岛屿数量"},{"content":"\n题目分析 从0~n-1遍历单词，每个单词与后续单词进行比较，如果不在同一集合且相似则合并。\n遍历两个字符串，记录不同位置的数量，如果不同位置的数量在2个以内则相似，超过了2个则不相似。\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public static int MAXN = 301; public static int[] father = new int[MAXN]; public static int sets; public static void build(int n){ for(int i=0;i\u0026lt;n;i++){ father[i]=i; } sets = n; } public static int find(int i){ if(i!=father[i]){ father[i]=find(father[i]); } return father[i]; } public static void union(int x,int y){ int fx = find(x); int fy = find(y); if(fx!=fy){ father[fx]=fy; sets--; } } public static int numSimilarGroups(String[] strs){ int n = strs.length; int m = strs[0].length(); build(n); for(int i=0;i\u0026lt;n;i++){ for(int j=i+1;j\u0026lt;n;j++){ if(find(i)!=find(j)){ int diff = 0; for(int k=0;k\u0026lt;m\u0026amp;\u0026amp;diff\u0026lt;3;k++){ if(strs[i].charAt(k)!=strs[j].charAt(k)){ diff++; } } if(diff==0||diff==2){ union(i, j); } } } } return sets; } ","date":"2026-01-03T21:34:46Z","permalink":"https://iamxurulin.github.io/p/leetcode839%E7%9B%B8%E4%BC%BC%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%BB%84/","title":"Leetcode839相似字符串组"},{"content":"\n代码求解 初始化并查集，每对情侣初始集合只有自身，遍历数组，计算相邻两人情侣编号并合并，合并之后集合数量减一，最后用总情侣对数减去集合数量得到结果。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public static int minSwapsCouples(int[] row){ int n = row.length; build(n/2); for(int i=0;i\u0026lt;n;i+=2){ union(row[i]/2,row[i+1]/2); } return n/2-sets; } public static int MAXN = 31; public static int[] father = new int[MAXN]; public static int sets; public static void build(int m){ for(int i=0;i\u0026lt;m;i++){ father[i]=i; } sets = m; } public static int find(int i){ if(i!=father[i]){ father[i]=find(father[i]); } return father[i]; } public static void union(int x,int y){ int fx = find(x); int fy = find(y); if(fx!=fy){ father[fx]=fy; sets--; } } ","date":"2026-01-03T20:56:51Z","permalink":"https://iamxurulin.github.io/p/leetcode765%E6%83%85%E4%BE%A3%E7%89%B5%E6%89%8B/","title":"Leetcode765情侣牵手"},{"content":" 代码求解 father数组： 每个集合元素往上的指针，下标对应节点编号，初始时每个节点指向自己。\nsize数组：记录每个集合的大小，初始时每个集合大小为1。\nstack数组 ： 用于扁平化过程中收集沿途节点。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 public static int MAXN = 1000001; public static int[] father = new int[MAXN]; public static int[] size = new int[MAXN]; public static int[] stack = new int[MAXN]; public static int n; public static void build(){ for(int i=0;i\u0026lt;=n;i++){ father[i]=i; size[i]=i; } } public static int find(int i){ int size = 0; while (i!=father[i]) { stack[size++]=i; i=father[i]; } while (size\u0026gt;0) { father[stack[--size]]=i; } return i; } public static boolean isSameSet(int x,int y){ return find(x)==find(y); } public static void union(int x,int y){ int fx = find(x); int fy = find(y); if (fx!=fy) { if(size[fx]\u0026gt;=size[fy]){ size[fx]+=size[fy]; father[fy]=fx; }else{ size[fy]+=size[fx]; father[fx]=fy; } } } public static void main(String[] args)throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); while (in.nextToken()!=StreamTokenizer.TT_EOF) { n = (int)in.nval; build(); in.nextToken(); int m = (int)in.nval; for(int i=0;i\u0026lt;m;i++){ in.nextToken(); int op = (int)in.nval; in.nextToken(); int x = (int)in.nval; in.nextToken(); int y = (int)in.nval; if(op==1){ out.println(isSameSet(x, y)?\u0026#34;Yes\u0026#34;:\u0026#34;No\u0026#34;); }else{ union(x, y); } } } out.flush(); out.close(); br.close(); } ","date":"2026-01-03T19:04:18Z","permalink":"https://iamxurulin.github.io/p/%E5%B9%B6%E6%9F%A5%E9%9B%86%E7%9A%84%E5%AE%9E%E7%8E%B0/","title":"并查集的实现"},{"content":"文件管理 ls 列出目录的内容 cd 切换目录 cp 复制文件或者目录 mv 移动或者重命名文件或者目录 rm 删除文件或者目录 touch 创建空文件或者更新文件的时间戳 find 查找文件或者目录 权限管理 chmod 修改文件或者目录的权限 chown 修改文件或者目录的所有者 chgrp 修改文件或者目录的所属组 文本处理 cat 查看文件的内容 grep 搜索文本的内容 sed 流编辑器，主要用于文本的处理 awk 文本处理工具，适用于处理列数据 系统管理 ps 显示当前的进程 top 动态地显示系统资源的使用情况 df 显示文件系统的磁盘空间使用情况 du 显示文件或者目录的磁盘空间使用情况 kill 终止进程 shutdown 关机或者重启系统 网络管理 ping 检测网络的连通性 ifconfig/ip 配置网络接口 netstat 显示网络连接、路由表等信息 ssh 远程登录 scp 安全复制文件 压缩和解压 tar 打包和解包文件 gzip 压缩和解压文件 zip/unzip 压缩和解压文件 ","date":"2026-01-03T16:40:32Z","permalink":"https://iamxurulin.github.io/p/%E6%95%B4%E7%90%86%E4%B8%80%E4%BA%9Blinux%E7%9A%84%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/","title":"整理一些Linux的常用命令"},{"content":"Java对象在虚拟机中主要由对象头、对象实例和对齐填充三部分组成。\n1.对象头中包含了对象的元信息和运行时数据。\n对象头主要由Mark Word、类型指针和数组长度三部分组成。\n其中，数组长度只有数组才有；\nMark Word主要用于存储运行时数据，会根据对象的状态动态变化；\n类型指针指向对象对应的类的元数据，用于确定该对象的类型。\n2.对象实例存储的是对象的实际数据，也就是类的字段。\n3.为了满足内存一般情况下的8字节对齐要求，JVM可能会在对象的末尾添加填充字节。\n至于存储位置，大多数的对象分配在堆中，堆也是JVM管理的内存中最大的一块区域。\n","date":"2026-01-03T16:19:53Z","permalink":"https://iamxurulin.github.io/p/java%E5%AF%B9%E8%B1%A1%E6%98%AF%E6%80%8E%E4%B9%88%E5%9C%A8%E8%99%9A%E6%8B%9F%E6%9C%BA%E4%B8%AD%E5%AD%98%E5%82%A8%E7%9A%84/","title":"Java对象是怎么在虚拟机中存储的"},{"content":"Linux CFS（ Completely Fair Scheduler，全公平调度器 ）是一种用于替代O(1)调度器的进程调度算法。\nCFS主要目的是使每个任务都能够按照其优先级，占用CPU的时间片段，尽可能公平地分配CPU资源。\nCFS有以下特点：\n1.每个任务都是按照优先级决定权重，进而根据权重分配CPU时间，尽量保证每个任务都能按比例公平地获得CPU资源。\n2.使用虚拟运行时间（vruntime）来衡量每个任务的CPU时间，vruntime越低，优先级越高。\n3.所有可运行的任务都存储在红黑树中，按照vruntime进行排序，其中最左节点也就是vruntime最小的节点更优先获得CPU的调度。\n4.CFS支持多核调度，能够很好地处理从嵌入式系统到服务器集群的各种应用场景。\n","date":"2026-01-03T15:53:48Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4linux-cfs/","title":"说说Linux-CFS"},{"content":"Spring是通过三级缓存机制来解决循环依赖的。\n我们可以把这三级缓存想象成3个不同等级的“货架”：\n其中，一级缓存存放的是成品货架，这里面都是完全初始化好，并且可以直接使用的Bean。\n二级缓存存放的是半成品货架，这里面都是已经实例化好的，但是呢，还没有填充属性的Bean。\n三级缓存存放的是工厂货架，这里放的就不是Bean了，而是一个可以生产Bean的工厂。\n我们来理一下三级缓存解决循环依赖的流程：\n假设A和B是相互依赖的，\n首先，Spring先把A实例化出来，此时A还只是一个空壳，紧接着把一个可以获取A的工厂放到三级缓存里。\n然后，A开始填充属性，这时发现需要B，所以就跑去创建B。\n这时，B也实例化出来了，也开始填充属性，发现需要A。\n然后，B就去一级缓存中寻找A，但是呢，没找到；所以又去二级缓存中寻找，也没找到；再跑去三级缓存中寻找，这下找到了。\n这时，B就调用三级缓存中的工厂，拿到了A的引用。\n为了保证是单例，只生产一次，B就把拿到的A放到二级缓存中，并把三级缓存中的工厂删掉。\nB拿到了A后，B就创建完成了，入驻一级缓存。\nA拿到B之后，A也可以创建完成，也可以入驻一级缓存。\n","date":"2026-01-03T15:15:01Z","permalink":"https://iamxurulin.github.io/p/spring%E6%98%AF%E6%80%8E%E4%B9%88%E8%A7%A3%E5%86%B3%E5%BE%AA%E7%8E%AF%E4%BE%9D%E8%B5%96%E7%9A%84/","title":"Spring是怎么解决循环依赖的"},{"content":"混合检索主要是为了提升大模型的上下文理解和回答的准确性，\n因为向量检索更擅长语义的理解，但是对于一些专有名词就难以做到精准匹配。\n而关键词检索的特性和向量检索相反，二者正好形成了互补优势，将两者结合能够大幅的提升检索结果的全面性和准确性。\n在大模型RAG应用中，混合检索主要通过向量检索将文本转化为高维向量，计算语义的相似度，同时并行基于倒排索引算法等关键词检索来精准地匹配关键词，最后将两种结果通过权重的融合或者重排序模型进行合并，最终输出最优的答案。\n","date":"2026-01-03T14:31:43Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4%E6%B7%B7%E5%90%88%E6%A3%80%E7%B4%A2/","title":"说说混合检索"},{"content":"\n问题分析 双端队列按照y-x的值从大到小组织，队列中存储点的编号。\n如果y-x的值大于队列尾部元素的y-x值，则从尾部弹出元素。\n如果当前点的x值与队列头部元素的x值之差大于k时，则从头部弹出元素。\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public static int MAXN = 100001; public static int[][] deque = new int[MAXN][2]; public static int h,t; public static int findMaxValueOfEquation(int[][] points,int k){ h = t = 0; int n = points.length; int ans = Integer.MIN_VALUE; for(int i=0,x,y;i\u0026lt;n;i++){ x=points[i][0]; y=points[i][1]; while (h\u0026lt;t\u0026amp;\u0026amp;deque[h][0]+k\u0026lt;x) { h++; } if(h\u0026lt;t){ ans = Math.max(ans, x+y+deque[h][1]-deque[h][0]); } while (h\u0026lt;t\u0026amp;\u0026amp;deque[t-1][1]-deque[t-1][0]\u0026lt;=y-x) { t--; } deque[t][0]=x; deque[t++][1]=y; } return ans; } ","date":"2026-01-02T23:20:11Z","permalink":"https://iamxurulin.github.io/p/leetcode1499%E6%BB%A1%E8%B6%B3%E4%B8%8D%E7%AD%89%E5%BC%8F%E7%9A%84%E6%9C%80%E5%A4%A7%E5%80%BC/","title":"Leetcode1499满足不等式的最大值"},{"content":"Java程序的执行流程：\n1.编写.java源代码文件。\n2.使用javac编译器生成.class字节码文件。\n3.通过java命令启动JVM，并指定主类。\n4.JVM类加载器按需加载主类及运行所需的其他.class文件。\n5.JVM定位到主类的main方法，开始执行其逻辑，作为程序的入口。\n6.执行过程中，JVM通过解释执行和JIT即时编译，将字节码转换为机器码并执行。\n7.运行期间，JVM对内存进行管理，回收不再使用的对象。\n8.如果没有非守护线程运行，则触发JVM退出流程。\n9.JVM销毁内部组件，释放资源，最终JVM进程退出。\n","date":"2026-01-02T16:32:54Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4java%E7%A8%8B%E5%BA%8F%E7%9A%84%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B/","title":"说说Java程序的执行流程"},{"content":"1.使用ps命令\n举个例子，如果要查看进程ID为1527的进程的内存使用情况，可以使用如下命令：\n1 ps -p 1527 -o %mem,%cpu,vsz,rss 其中，vsz表示虚拟内存的大小，rss表示常驻内存的大小。\n2.使用top命令\n要查看某个进程的内存使用情况时，可以依次输入top，再按f键进入域选择界面，再按N键就能显示内存的使用排序。\n3.使用/proc文件系统\n/proc提供了内核和进程的信息。\n每个进程都有一个对应的目录，以ID为1527的进程为例，\n/proc/1527/status包含VmSize虚拟内存大小和VmRSS常驻内存大小这些基本信息；\n/proc/1527/statm包含进程的内存页面数。\n","date":"2026-01-02T15:53:05Z","permalink":"https://iamxurulin.github.io/p/%E5%A6%82%E4%BD%95%E5%9C%A8linux%E7%B3%BB%E7%BB%9F%E4%B8%AD%E6%9F%A5%E7%9C%8B%E6%9F%90%E4%B8%AA%E7%89%B9%E5%AE%9A%E8%BF%9B%E7%A8%8B%E7%9A%84%E5%86%85%E5%AD%98%E4%BD%BF%E7%94%A8%E6%83%85%E5%86%B5/","title":"如何在Linux系统中查看某个特定进程的内存使用情况"},{"content":"在Java8以前，方法区被实现在永久代中，永久代是一块固定大小地内存区域，这块区域不能进行动态地扩展。所以，如果加载的类过多或者常量池的数据过多，超出了永久代的限制，就会报永久代内存溢出的错误。\n在Java 8及之后，方法区被实现在元空间中，不再使用堆内存，转向使用本地内存，元空间可以通过参数设置最大大小，但也会受物理内存的限制。\n所以，如果加载的类过多或者大量动态生成类还是会报元空间内存溢出的错误。\n","date":"2026-01-02T15:19:31Z","permalink":"https://iamxurulin.github.io/p/jvm%E6%96%B9%E6%B3%95%E5%8C%BA%E4%BC%9A%E5%87%BA%E7%8E%B0%E5%86%85%E5%AD%98%E6%BA%A2%E5%87%BA%E5%90%97/","title":"JVM方法区会出现内存溢出吗"},{"content":"昨天说了Spring Boot 3.x和2.x版本相比有哪些区别与改进？，今天来看看Spring Boot 2.x和1.x版本相比有哪些区别与改进？\n1.Spring Boot 1.x基于Spring Framework 4.x，不支持响应式编程；Spring Boot 2.x基于Spring Boot 5，引入了对响应式编程的支持。\n2.Spring Boot 2.x对Tomcat、Jetty这些嵌入式Web容器的默认版本进行了升级，带来了对新HTTP标准的支持。\n3.Spring Boot 2.x对底层组件和框架本身做了大量的性能优化，使得应用启动时间更短，运行性能更高，更加适合云原生应用和大规模的微服务架构。\n4.Spring Boot 1.x中，Actuator端点是默认全部开启的，存在安全上的隐患，Spring Boot 2.x对Actuator进行了全面改进，在默认情况下，大多数端点是关闭的，开发者可以通过配置显示地启动需要的端点。\n","date":"2026-01-02T15:02:23Z","permalink":"https://iamxurulin.github.io/p/spring-boot-2.x%E5%92%8C1.x%E7%89%88%E6%9C%AC%E7%9B%B8%E6%AF%94%E6%9C%89%E5%93%AA%E4%BA%9B%E5%8C%BA%E5%88%AB%E4%B8%8E%E6%94%B9%E8%BF%9B/","title":"Spring-Boot-2.x和1.x版本相比有哪些区别与改进"},{"content":"A2A（Agent2Agent）协议是为了打破现阶段各个Agent框架之间的隔离，让它们可以实现无障碍地交流而提出的一种AI智能体界的普通话标准。\nA2A协议遵循5大设计原则：\n1.传统的API是我调用你一下，你就响应一下。但A2A是把Agent当人看，而不是当工具来用。它允许Agent之间像同事一样进行协作，它们可以自己商量怎么完成任务。（自主性）\n2.A2A协议是建立在现有标准之上的，底层用的是HTTP、SSE、JSON-RPC这些成熟的技术，所以企业现有的IT系统可以直接接入。（标准化）\n3.A2A集成了企业级的身份验证和授权，和OpenAPI的安全标准看齐，可以确保Agent之间的交互是可控和安全的。（安全性）\n4.有些AI任务，像写代码或者做研究这种，可能要跑好几个小时甚至好几天才能出结果，为了让用户不用傻等，能够随时查看状态，A2A设计了异步机制，通过SSE可以实时反馈进度。（异步性）\n5.如果Agent只能发文字，而不能发语音和视频，就显得有些单调了。因此，A2A原生支持音频和视频流传输，为未来的语音助手和视频分析Agent预留了接口。（多模态）\n","date":"2026-01-02T14:25:49Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4a2a%E5%8D%8F%E8%AE%AE%E7%9A%845%E5%A4%A7%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99/","title":"说说A2A协议的5大设计原则"},{"content":"\n题目分析 遍历数组，对于每个位置i，假设子数据必须以i作为右边界，求出往左延伸多短，能够让累加和大于等于K，最后从所有答案中选择最小值。\n准备一个双端队列，要求从头到尾严格小到大。 当来到位置i，0~i范围的前缀和为x时，从头部依次考察，如果x减去队列头部指示的前缀和达标，则记录答案并将头部元素弹出，因为后续位置使用该前缀和得到的结果不会更短。\n加入新的前缀和时，如果不满足队列小到大的顺序，则从尾部弹出元素。\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static int MAXN = 100001; public static long[] sum = new long[MAXN]; public static int[] deque = new int[MAXN]; public static int h,t; public static int shortestSubarray(int[] arr,int K){ int n = arr.length; for(int i = 0;i\u0026lt;n;i++){ sum[i+1]=sum[i]+arr[i]; } h=t=0; int ans = Integer.MAX_VALUE; for(int i=0;i\u0026lt;=n;i++){ while (h!=t\u0026amp;\u0026amp;sum[i]-sum[deque[h]]\u0026gt;=K) { ans = Math.min(ans, i-deque[h++]); } while (h!=t\u0026amp;\u0026amp;sum[deque[t-1]]\u0026gt;=sum[i]) { t--; } deque[t++]=i; } return ans!=Integer.MAX_VALUE?ans:-1; } ","date":"2026-01-01T21:49:49Z","permalink":"https://iamxurulin.github.io/p/leetcode862%E5%92%8C%E8%87%B3%E5%B0%91%E4%B8%BAk%E7%9A%84%E6%9C%80%E7%9F%AD%E5%AD%90%E6%95%B0%E7%BB%84/","title":"Leetcode862和至少为K的最短子数组"},{"content":"\n题目分析 如果一个子数组达标，也就是它内部的最大值减去最小值小于等于给定限制limit， 则其内部任意小范围一定也达标，因为范围变小，最大值只可能变小或者不变，而最小值只可能变大或者不变。\n如果一个子数组不达标，则再扩大范围一定会更加不达标，因为范围变大，最大值只可能上升或者不变，而最小值只可能下降或者不变。\n先固定左边界l，右边界r不断右移扩展窗口，每次判断新加入的数是否使窗口达标，如果达标，则继续拓展，不达标则停止，计算以当前l开头的达标子数组的最大长度。\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public static int MAXN = 100001; public static int[] maxDeque = new int[MAXN]; public static int[] minDeque = new int[MAXN]; public static int maxh,maxt,minh,mint; public static int[] arr; public static int longestSubarray(int[] nums,int limit){ maxh = maxt = minh = mint = 0; arr = nums; int n = arr.length; int ans = 0; for(int l=0,r=0;l\u0026lt;n;l++){ while(r\u0026lt;n\u0026amp;\u0026amp;ok(limit,nums[r])){ push(r++); } ans = Math.max(ans, r-l); pop(l); } return ans; } public static boolean ok(int limit,int number){ int max = maxh \u0026lt;maxt?Math.max(arr[maxDeque[maxh]], number):number; int min = minh \u0026lt;mint?Math.min(arr[minDeque[minh]], number):number; return max-min\u0026lt;=limit; } public static void push(int r){ while (maxh\u0026lt;maxt\u0026amp;\u0026amp;arr[maxDeque[maxt-1]]\u0026lt;=arr[r]) { maxt--; } maxDeque[maxt++]=r; while (minh\u0026lt;mint\u0026amp;\u0026amp;arr[minDeque[mint-1]]\u0026gt;=arr[r]) { mint--; } minDeque[mint++]=r; } public static void pop(int l) { if (maxh \u0026lt; maxt \u0026amp;\u0026amp; maxDeque[maxh] == l) { maxh++; } if (minh \u0026lt; mint \u0026amp;\u0026amp; minDeque[minh] == l) { minh++; } }\t","date":"2026-01-01T20:50:33Z","permalink":"https://iamxurulin.github.io/p/leetcode1438%E7%BB%9D%E5%AF%B9%E5%80%BC%E4%B8%8D%E8%B6%85%E8%BF%87%E9%99%90%E5%88%B6%E7%9A%84%E6%9C%80%E9%95%BF%E8%BF%9E%E7%BB%AD%E5%AD%90%E6%95%B0%E7%BB%84/","title":"Leetcode1438绝对值不超过限制的最长连续子数组"},{"content":"编译执行是程序在执行之前，先通过编译器将源代码编译为机器代码，然后直接在CPU上运行；\n解释执行是源代码在不经过编译器编译的前提下，直接在运行的时候通过解释器逐行翻译并执行。\n常见的编译性语言有C和C++，而常见的解释性语言有Python。\n编译执行的语言因为编译后的程序不需要在运行的时候再进行翻译，所以运行速度快。\n但是，程序需要针对每个平台重新编译，跨平台性会更差一点。\n而解释执行的语言在每个平台上都是通过相应平台的解释器来运行的，跨平台性好。但是每次执行的时候都需要进行动态的翻译和解释，所以运行速度更慢。\n严格来说，JVM是结合了编译执行和解释执行的。\n正常情况下JVM是解释执行的，不过，如果JVM发现某段逻辑执行的特别频繁，那么它就会通过JIT（Just In Time）即时编译将其编译成机器码，这样就是编译执行了。\n","date":"2026-01-01T17:35:59Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4%E7%BC%96%E8%AF%91%E6%89%A7%E8%A1%8C%E5%92%8C%E8%A7%A3%E9%87%8A%E6%89%A7%E8%A1%8C%E7%9A%84%E5%8C%BA%E5%88%AB/","title":"说说编译执行和解释执行的区别"},{"content":"跨平台指的是在不同的硬件或者操作系统上，Java代码在不需要针对不同的平台做对应的修改的前提下，都可以正常运行。\n要实现这样一种一次编写到处运行的特性，主要靠的是JVM，即Java虚拟机。\n和其他编程语言在编译后直接生成特定于某一操作系统的二进制01机器代码不同，Java程序在编译之后生成的是.class格式的字节码。\nJVM为了屏蔽不同硬件或操作系统的底层细节，针对不同的平台做了对应的开发，可以实现将字节码翻译成特定平台上的机器代码并成功执行，这就使得同一份Java字节码可以在任意的支持JVM的平台上正常运行。\n","date":"2026-01-01T17:02:43Z","permalink":"https://iamxurulin.github.io/p/java%E6%98%AF%E6%80%8E%E4%B9%88%E5%AE%9E%E7%8E%B0%E8%B7%A8%E5%B9%B3%E5%8F%B0%E7%9A%84/","title":"Java是怎么实现跨平台的"},{"content":"Rerank其实一个是对初步检索返回的候选文档列表再次进行排序的过程。\n如果把RAG的检索过程类比成公司的招聘过程，则有如下对应关系：\n1.初步检索（Retrieval） 这一步就类似于HR筛选简历，找工作的人往往很多，HR每天都可能收到上万份甚至几万份简历。\n假设有1万份简历，1个HR每天工作8小时，那么1小时之内就得看完1250份，平均到1分钟以内就得看完20多份，这几乎是不可能的。\n所以，不可能每份简历HR都会细看。\n那她会怎么看呢，主要就是看关键词，比如Java、 大模型等等，看到简历上有这些匹配的关键词字眼 ，就筛选出来。\n这样做就会导致看似简历已经匹配上关键词了，但是候选人的能力可能还不太符合要求，只能选出前100个看似还行的候选人。\n我们把这个场景对应到RAG的检索过程里面就是，向量检索，它虽然算得快，但是对语义的理解还差点意思。\n2.重排序（Rerank）\n这个过程就类似于企业招聘过程的业务面试，也就是主管把这100人的简历打印出来仔细阅读，如果觉得候选人的过往经历和工作经验很符合要求，就会约下一步的一对一业务面试。\n这个过程花费的时间比较长，消耗的精力也更多，但是筛选出来的候选人也更符合要求。\n这也就是RAG检索过程的Rerank，可以精准地判断初步检索返回的文档是不是真正能够回答用户的问题，最后再喂给大模型去生成一个答案。\n","date":"2026-01-01T16:44:33Z","permalink":"https://iamxurulin.github.io/p/%E8%A7%A3%E9%87%8A%E4%B8%80%E4%B8%8Brag%E4%B8%AD%E7%9A%84rerank/","title":"解释一下RAG中的Rerank"},{"content":"1.Spring Boot 2.x基于Java EE，而Spring Boot 3.x迁移到了Jakartaa EE，一些核心的包名也从javax.*变更为jakarta.*了。\n2.Spring Boot 2.x支持JDK8、11和17版本，而Spring Boot 3.x要求JDK版本最低为17。\n3.Spring Boot 2.x 没有原生编译的内置支持，Spring Boot3.x则提供了对GraalVM Native Image的开箱即用支持，可以将Spring应用编译成本地的可执行文件。\n4.Spring Boot 2.x仅支持基本的监控和追踪，Spring Boot 3.x引入了更完善的分布式追踪、日志关联和性能指标收集，支持OpenTelemetry标准，开发者可以借助Observability更好地监控和分析应用的运行状况。\n5.Spring Boot 2.x支持Spring Security 5，在安全性上存在一定的局限性；Spring Boot 3.x增强了对Spring Security 6的支持，强化了身份认证、授权和安全配置的能力。\n6.Spring Boot 2.x的依赖库和自动配置很强大，但是存在一些历史遗留的依赖和配置；Spring Boot 3.x对内部依赖进行了一些模块化的调整，对一些不再使用或者过时的库进行了清理。\n","date":"2026-01-01T16:05:33Z","permalink":"https://iamxurulin.github.io/p/spring-boot-3.x%E5%92%8C2.x%E7%89%88%E6%9C%AC%E7%9B%B8%E6%AF%94%E6%9C%89%E5%93%AA%E4%BA%9B%E5%8C%BA%E5%88%AB%E4%B8%8E%E6%94%B9%E8%BF%9B/","title":"Spring-Boot-3.x和2.x版本相比有哪些区别与改进"},{"content":"有多种原因可造成@Async失效：\n1.@Async依赖于Spring AOP，如果是内部调用的话则会绕过代理对象，直接调用原始方法。\n2.Spring AOP默认只会对public方法生效，对于非public方法则不会被代理，所以此时@Async是失效的。\n3.如果Spring Boot的主类或者配置类上没有添加@EnableAsync注解，也就是没有显示启用异步功能，则Spring是不会为其生成代理的。\n4.@Async依赖于Spring容器管理的Bean，如果是手动new的对象，并没有被Spring管理，则代理机制也是会失效的。\n","date":"2026-01-01T14:12:06Z","permalink":"https://iamxurulin.github.io/p/%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99@async%E4%BC%9A%E5%A4%B1%E6%95%88/","title":"什么时候@Async会失效"},{"content":"\n求解代码 准备一个数组作为双端队列deque，用h（head）和t（tail）表示队列的头和尾位置。\n双端队列要维持从左往右大到小的顺序。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static int MAXN = 100001; public static int[] deque = new int[MAXN]; public static int h,t; public static int[] maxSlidingWindow(int[] arr,int k){ h = t = 0; int n = arr.length; for(int i=0;i\u0026lt;k-1;i++){ while(h\u0026lt;t\u0026amp;\u0026amp;arr[deque[t-1]]\u0026lt;=arr[i]){ t--; } deque[t++]=i; } int m = n-k+1; int[] ans = new int[m]; for(int l=0,r=k-1;l\u0026lt;m;l++,r++){ while(h\u0026lt;t\u0026amp;\u0026amp;arr[deque[t-1]]\u0026lt;=arr[r]){ t--; } deque[t++]=r; ans[l]=arr[deque[h]]; if(deque[h]==l){ h++; } } return ans; } ","date":"2025-12-31T21:20:32Z","permalink":"https://iamxurulin.github.io/p/leetcode239%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC/","title":"Leetcode239滑动窗口最大值"},{"content":"栈主要用于存储局部变量和方法的返回地址、参数等调用信息。\n堆主要用于存储对象实例和数组。\nJVM的垃圾回收主要是针对堆空间的一个处理，而栈空间是不会被回收的。\n在一次方法的调用过程中，调用的时候数据存入栈空间，当方法执行完之后，数据就被弹出释放，\n不需要像堆空间一样等待GC进行回收，所以相对而言，栈空间的生命周期会比堆空间更短。\n","date":"2025-12-31T16:04:55Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E4%B8%80%E8%AF%B4java%E4%B8%AD%E5%A0%86%E5%92%8C%E6%A0%88%E7%9A%84%E5%8C%BA%E5%88%AB/","title":"说一说Java中堆和栈的区别"},{"content":"要想实现在Spring Boot工程启动之后，直接将数据库中已有的固定内容打入到Redis缓存中，首先需要确保实体类已经实现了Serializable接口，并且已经正确配置了RedisTemplate的序列化方式 。\n可以考虑使用CommanLineRunner或者ApplicationRunner接口，在应用启动完成后执行初始化逻辑；\n也可以通过@PostConstruct注解，在Bean创建之后立即执行数据库加载。\n在懒加载场景，可以使用@Cacheable注解，在首次调用方法时触发缓存写入，完成预加载。\n","date":"2025-12-31T15:49:46Z","permalink":"https://iamxurulin.github.io/p/spring-boot%E5%B7%A5%E7%A8%8B%E5%90%AF%E5%8A%A8%E4%BB%A5%E5%90%8E-%E6%80%8E%E4%B9%88%E5%B0%86%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%AD%E5%B7%B2%E6%9C%89%E7%9A%84%E5%9B%BA%E5%AE%9A%E5%86%85%E5%AE%B9%E6%89%93%E5%85%A5%E5%88%B0redis%E7%BC%93%E5%AD%98%E4%B8%AD/","title":"Spring-Boot工程启动以后-怎么将数据库中已有的固定内容打入到Redis缓存中"},{"content":"我们可以把大模型想象成一个学霸，这个学霸有两个缺点，一个是他的知识存在滞后性，还有一个就是他不懂一些内部的机密。\nRAG（Retrieval Augmented Generation）检索增强生成，其实就是为了解决这两个问题而出现的。\nRAG的流程就是检索➡️增强➡️生成：\n1.当用户提问时，先不急着去问大模型，而是先去向量数据库中，把和这个问题相关的资料片段全部给找出来；（检索）\n2.接下来，把用户提的问题和第1步找到的资料片段缝合在一起，变成一个新的、信息量也更大的提示词；（增强）\n3.把第2步生成的提示词喂给大模型，这个时候大模型再根据我们提供的资料，可以生成一个更为准确的答案。（生成）\n","date":"2025-12-31T15:12:08Z","permalink":"https://iamxurulin.github.io/p/%E8%A7%A3%E9%87%8A%E4%B8%80%E4%B8%8Brag/","title":"解释一下RAG"},{"content":"为了防止被恶意刷量，本能地想到要用限流。\n当然，除了限流，还有其他的一些方式：\n1.在前后端约定一套加密算法和一个密钥。\n在前端把所有参数按照字母进行排序，再加上密钥生成一个签名，将其放在请求头里传给后端。\n在后端用和前端一样的规则生成一个签名，如果两个签名对不上，很明显参数在传输的过程中被篡改了，可以直接拒绝，从而防止攻击者随意地修改参数来刷接口。\n2.如果攻击者截获可一个合法的请求包，在改不了参数的懊恼下，他直接疯狂地重复发送这个包。这种攻击方式的解决办法是：\n在后端先对时间戳进行校验，如果是超过了60秒的请求，则直接丢弃；\n如果请求是在60秒以内的，再检查随机数是否在Redis中已经存在，如果是已经存在，说明这是个重复请求，直接拦截掉。\n3.当系统检测到某个用户存在频率稍高，但是还没到限流阈值的异常行为或者在进行注册、领券等较为敏感的操作时，强制弹出滑块验证码，从而对人和脚本进行区分。\n4.如果发现某个IP存在于IP黑名单或者用户的ID黑名单中，可以直接在网关层对这个IP进行封禁，不让它进入到业务层。\n","date":"2025-12-31T14:53:21Z","permalink":"https://iamxurulin.github.io/p/%E5%A6%82%E4%BD%95%E9%98%B2%E6%AD%A2%E6%8E%A5%E5%8F%A3%E8%A2%AB%E6%81%B6%E6%84%8F%E5%88%B7%E9%87%8F/","title":"如何防止接口被恶意刷量"},{"content":"为了防止被短信验证码恶意轰炸，可以从两方面考虑：一是增加攻击者的成本，再就是降低攻击者的收益。\n可以通过以下几种方式实现：\n1.在调用发送短信的接口之前，需要先校验图形验证码，只有图形验证码校验通过了，才允许请求短信接口，这种方式可以拦截大量的脚本自动刷量。\n2.基于Redis对单个手机号进行限制，比如限制1分钟内只能发送1条,1小时以内最多发送5条或者24小时以内最多发送10条。\n3.为了防止有人换手机号进行轰炸，可以对IP进行限制，同一个IP，限制在24小时内最多发送20条短信。一旦该IP的短信发送量超过这个阈值，直接对其进行封禁处理。\n4.如果是国内的业务，则可以一刀切，直接把所有境外的号码屏蔽掉。\n","date":"2025-12-31T14:16:30Z","permalink":"https://iamxurulin.github.io/p/%E5%A6%82%E4%BD%95%E9%98%B2%E6%AD%A2%E8%A2%AB%E7%9F%AD%E4%BF%A1%E9%AA%8C%E8%AF%81%E7%A0%81%E6%81%B6%E6%84%8F%E8%BD%B0%E7%82%B8/","title":"如何防止被短信验证码恶意轰炸"},{"content":"VMware虚拟机中的Ubuntu安装Docker后，输入：\n1 sudo docker run hello-world 测试Docker是否安装成功。\n出现以下问题： 这个错误是因为 Docker 无法连接到 Docker registry 来拉取镜像。\n通过更换国内镜像源解决，创建 Docker 配置文件：\n1 sudo vim /etc/docker/daemon.json 添加国内镜像源：\n1 2 3 4 5 6 7 8 { \u0026#34;registry-mirrors\u0026#34;: [ \u0026#34;https://docker.m.daocloud.io\u0026#34;, \u0026#34;https://dockerproxy.com\u0026#34;, \u0026#34;https://docker.mirrors.ustc.edu.cn\u0026#34;, \u0026#34;https://docker.nju.edu.cn\u0026#34; ] } 重启 Docker:\n1 2 sudo systemctl daemon-reload sudo systemctl restart docker 验证配置:\n1 2 # 查看配置是否生效 sudo docker info | grep -A 5 \u0026#34;Registry Mirrors\u0026#34; 出现以下信息：\n说明配置已经生效了。\n最后再测试运行：\n1 sudo docker run hello-world 看到有如下“Hello from Docker!”的输出，表明Docker已经安装成功了，并且可以拉取镜像去执行了。\n","date":"2025-12-31T08:38:06Z","permalink":"https://iamxurulin.github.io/p/docker-error-response-from-daemon-get-https-registry-1.docker.io-v2-net-%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95/","title":"docker-Error-response-from-daemon-Get-https-registry-1.docker.io-v2-net-解决办法"},{"content":"\n代码求解 栈是一个n*2的二维数组，每个元素包含两个数，0位置的是体重，1位置的是轮数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public static int MAXN = 100001; public static int[] arr = new int[MAXN]; public static int n; public static int[][] stack = new int[MAXN][2]; public static int r; public static void main(String[] args)throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); while(in.nextToken()!=StreamTokenizer.TT_EOF){ n = (int)in.nval; for(int i=0;i\u0026lt;n;i++){ in.nextToken(); arr[i]=(int)in.nval; } out.println(turns()); } out.flush(); out.close(); br.close(); } public static int turns(){ r = 0; int ans = 0; for(int i=n-1,curTurns;i\u0026gt;=0;i--){ curTurns = 0; while(r\u0026gt;0\u0026amp;\u0026amp;stack[r-1][0]\u0026lt;arr[i]){ curTurns = Math.max(curTurns+1, stack[--r][1]); } stack[r][0]=arr[i]; stack[r++][1]=curTurns; ans = Math.max(ans, curTurns); } return ans; } ","date":"2025-12-30T22:03:56Z","permalink":"https://iamxurulin.github.io/p/%E5%8D%95%E8%B0%83%E6%A0%88-%E5%A4%A7%E9%B1%BC%E5%90%83%E5%B0%8F%E9%B1%BC%E9%97%AE%E9%A2%98/","title":"单调栈-大鱼吃小鱼问题"},{"content":"\n求解思路 构建大压小的单调栈。\n建立词频表，统计字符串中每种字符出现的次数。\n新字符入栈时，如果破坏了大压小的原则，看栈顶元素对应的字符在后面是否还有，如果有则栈顶元素出栈，新字符入栈；\n如果栈里已有该字符，则直接跳过。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public static int MAXN = 26; public static int[] cnts = new int[MAXN]; public static boolean[] enter = new boolean[MAXN]; public static char[] stack = new char[MAXN]; public static int r; public static String removeDuplicateLetters(String str){ r=0; Arrays.fill(cnts, 0); Arrays.fill(enter, false); char[] s = str.toCharArray(); for(char cha:s){ cnts[cha-\u0026#39;a\u0026#39;]++; } for(char cur:s){ if(!enter[cur-\u0026#39;a\u0026#39;]){ while(r\u0026gt;0\u0026amp;\u0026amp;stack[r-1]\u0026gt;cur\u0026amp;\u0026amp;cnts[stack[r-1]-\u0026#39;a\u0026#39;]\u0026gt;0){ enter[stack[r-1]-\u0026#39;a\u0026#39;]=false; r--; } stack[r++]=cur; enter[cur-\u0026#39;a\u0026#39;]=true; } cnts[cur-\u0026#39;a\u0026#39;]--; } return String.valueOf(stack,0,r); } ","date":"2025-12-30T21:08:58Z","permalink":"https://iamxurulin.github.io/p/leetcode316%E5%8E%BB%E9%99%A4%E9%87%8D%E5%A4%8D%E5%AD%97%E6%AF%8D/","title":"Leetcode316去除重复字母"},{"content":"\n问题分析 这道题采用小压大的单调栈求解。\n从左往右遍历数组，只有依次小压大这种下降趋势的下标才有必要收集进单调栈。\n收集完单调栈后，从右往左遍历数组。\n假设当前的右侧位置为最右下标，依次看其与栈中的左侧下标是否能构成坡。\n如果可以构成，则计算宽度并更新最大宽度答案，同时弹出该左侧下标。\n初始化栈的时候，栈的大小为1，相当于是把0下标放入栈中。\n求解代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static int MAXN = 50001; public static int[] stack = new int[MAXN]; public static int r; public static int maxWidthRamp(int[] arr){ r =1; int n = arr.length; for(int i=1;i\u0026lt;n;i++){ if(arr[stack[r-1]]\u0026gt;arr[i]){ stack[r++]=i; } } int ans = 0; for(int j=n-1;j\u0026gt;=0;j--){ while(r\u0026gt;0\u0026amp;\u0026amp; arr[stack[r-1]]\u0026lt;=arr[j]){ ans = Math.max(ans,j-stack[--r]); } } return ans; } ","date":"2025-12-30T20:05:05Z","permalink":"https://iamxurulin.github.io/p/leetcode962%E6%9C%80%E5%A4%A7%E5%AE%BD%E5%BA%A6%E5%9D%A1/","title":"Leetcode962最大宽度坡"},{"content":"滑动验证码应该大家都不陌生，在登录的时候都基本上遇到过，那怎么实现这样一个功能呢？\n核心其实就两个，一个是后端造图，另一个是行为校验。\n在后端随机选择一张背景图，再随机选择一个位置(X,Y)，利用OpenCV或者Java的BufferedImage算法扣出一块拼图。\n然后，在后端把背景图和拼图块转成Base64给前端，同时把正确的X坐标存到Redis中。\n前端主要监听鼠标或者触摸事件，当用户拖动滑块时，前端不仅要记录最终滑动的距离，还要记录整个滑动轨迹。\n当鼠标松开时，再把数据发给后端。\n为了防止被机器识别破解，需要在后端进行双重校验。\n第一重是判断前端传来的X偏移量和Redis中存储的X是否匹配；\n第二重则是对轨迹进行分析，如果轨迹太过完美，可以直接判定为机器人。\n因为一般来说，机器人的轨迹是匀速的，直线的 或者是一条完美的数学曲线。\n但是，人的轨迹就很难做到这么完美，可能会有抖动，先快后慢或者还可能有回退。\n","date":"2025-12-30T15:35:50Z","permalink":"https://iamxurulin.github.io/p/%E6%80%8E%E4%B9%88%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E6%BB%91%E5%8A%A8%E9%AA%8C%E8%AF%81%E7%A0%81%E5%8A%9F%E8%83%BD-%E5%8F%88%E5%A6%82%E4%BD%95%E9%98%B2%E6%AD%A2%E8%A2%AB%E6%9C%BA%E5%99%A8%E8%AF%86%E5%88%AB%E7%A0%B4%E8%A7%A3/","title":"怎么实现一个滑动验证码功能-又如何防止被机器识别破解"},{"content":"crontab是Linux系统中的一个定时任务调度器，能够允许用户在一个特定的时间执行某些任务。\ncrontab指令通常是由5个时间字段和1个命令字段组成：\n5个时间字段分别是：\n分钟 0~59 小时 0~23 日期 1~31 月份 1~12 星期几 0~7 其中，0和7都表示星期日。\n如果想要在凌晨2点执行一个脚本，可以执行如下crontab指令：\n1 0 2 * * * /path/to/script.sh ","date":"2025-12-30T15:07:15Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4linux%E7%B3%BB%E7%BB%9F%E4%B8%ADcrontab%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/","title":"说说Linux系统中crontab的工作原理"},{"content":"我们先明确定义：\n进程是一个具有独立运行环境的程序实例，操作系统可以通过进程调度来管理CPU的时间分配。\n而线程则是进程中一个单独的执行路径，并且一个进程中可以包含多个线程，这些线程共享进程的地址空间和其他资源。\n主要区别在于：\n1.进程是资源分配的基本单位，线程是CPU调度的基本单位。\n2.进程与进程之间是相互独立的，也就是说一个进程的崩溃并不会对其他的进程造成影响。但是，线程之间因为是共享进程的资源的，所以，如果一个线程崩溃了，可能会导致整个进程的崩溃。\n3.进程之间的切换开销是大于线程之间的切换的，因为线程是在同一进城内切换，所以会相对轻松点。\n4.进程主要应用于Web服务器和数据库管理系统这些场景，线程则主要应用于多线程下载器和并发处理计算任务的场景，比如Java线程池等等。\n","date":"2025-12-30T14:50:54Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E4%B8%80%E8%AF%B4linux%E7%B3%BB%E7%BB%9F%E4%B8%AD%E8%BF%9B%E7%A8%8B%E5%92%8C%E7%BA%BF%E7%A8%8B%E7%9A%84%E5%8C%BA%E5%88%AB/","title":"说一说Linux系统中进程和线程的区别"},{"content":"1.top命令 top命令可以实时地查看当前系统CPU使用率、内存使用率、进场ID等信息。\n2.free命令 free命令可以查看系统中包括总内存量、已使用内存量和可用内存量等内存使用情况。\n3.df命令 df命令可以查看系统中包括磁盘总容量、已使用空间和可用空间等磁盘使用情况。\n4.iostat命令 iostat命令可以查看当前系统的CPU和I/O使用情况。\n5.netstat命令 netstat命令可以查看系统中像本地地址、远程地址、连接状态等网络连接情况。\n6.lsof命令 lsof命令可以查看当前系统中打开的文件和网络连接情况，像文件名、文件扫描符、进程ID、进程名等等。\n","date":"2025-12-30T14:29:00Z","permalink":"https://iamxurulin.github.io/p/%E5%9C%A8linux%E4%B8%AD%E5%A6%82%E4%BD%95%E6%9F%A5%E7%9C%8B%E7%B3%BB%E7%BB%9F%E4%B8%AD%E5%86%85%E5%AD%98cpu%E5%92%8C%E7%BD%91%E7%BB%9C%E7%AB%AF%E5%8F%A3%E7%9A%84%E4%BD%BF%E7%94%A8%E6%83%85%E5%86%B5/","title":"在Linux中如何查看系统中内存、CPU和网络端口的使用情况"},{"content":"CC攻击和DDOS攻击都是一种网络攻击方式。\nCC 攻击是攻击者使用大量的机器或者网络中的代理服务器，向某个目标服务器发送大量的请求，以消耗服务器的带宽和资源，导致服务器瘫痪。\nDDOS攻击（Distributed Denial of Service Attack）也是利用大量的计算机或者网络中的代理服务器，同时向某个目标服务器发送大量的请求，导致服务器瘫痪。\nCC攻击其实是DDOS攻击的一种特定的形式，专门针对Web应用程序的一种攻击。\n网站数据库注入则是一种利用Web应用程序漏洞的攻击方式。\n这种攻击方式，攻击者通过将恶意SQL代码插入到Web应用程序的输入字段，对数据库进行未授权访问，从而破坏数据库的完整性、泄露敏感数据等等。\n","date":"2025-12-30T14:15:03Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4cc%E6%94%BB%E5%87%BBddos%E6%94%BB%E5%87%BB%E5%92%8C%E7%BD%91%E7%AB%99%E6%95%B0%E6%8D%AE%E5%BA%93%E6%B3%A8%E5%85%A5/","title":"说说CC攻击、DDOS攻击和网站数据库注入"},{"content":"因为官方 Spring Initializr 服务（start.spring.io）已停止对 Spring Boot 2.x 系列旧版本的支持，仅保留 3.5.0 及以上新版本的分发，因此无法通过该服务获取 2.x 版本的项目模板。 并且Spring Boot 3.x 强制要求 Java ≥17，只有 2.x 系列支持 Java 8，所以会导致下拉框选不到Java8。\n为了解决这个问题，需要将在IDEA 的 Spring Initializr 界面，点击顶部「Server URL」输入框，删除原有地址，输入阿里云的服务地址：\n1 https://start.aliyun.com 之后就能够选择Java 8了。\n点击Next之后，也能够选择2.X 系列的Spring Boot版本。 ","date":"2025-12-30T09:40:44Z","permalink":"https://iamxurulin.github.io/p/idea2023%E4%B8%AD%E6%96%B0%E5%BB%BAspring-boot2.x%E7%89%88%E6%9C%AC%E7%9A%84%E5%B7%A5%E7%A8%8B%E7%9A%84%E6%96%B9%E6%B3%95/","title":"IDEA2023中新建Spring-Boot2.X版本的工程的方法"},{"content":" 代码求解 对于直方图中的每个高度，找到其左右两侧离它最近且比它小的高度位置，以该高度为高向左右两侧拓展，计算拓展的单位数，再乘以该高度得到长方形面积，对每个高度进行遍历，求得最大值。\n虽然高度相等的时候弹出的计算结果可能是错误的，但总是会有最后一个相同高度能够算对，所以并不会影响最终求解的最大长方形的面积。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Solution { public static int MAXN = 100001; public static int[] stack = new int[MAXN]; public static int r; public static int largestRectangleArea(int[] height){ int n = height.length; r = 0; int ans = 0,cur,left; for(int i = 0;i\u0026lt;n;i++){ while(r\u0026gt;0\u0026amp;\u0026amp;height[stack[r-1]]\u0026gt;=height[i]){ cur = stack[--r]; left = r==0?-1:stack[r-1]; ans = Math.max(ans, height[cur]*(i-left-1)); } stack[r++]=i; } while (r\u0026gt;0) { cur = stack[--r]; left = r==0?-1:stack[r-1]; ans = Math.max(ans, height[cur]*(n-left-1)); } return ans; } } ","date":"2025-12-29T23:26:48Z","permalink":"https://iamxurulin.github.io/p/%E5%8D%95%E8%B0%83%E6%A0%88-%E6%9F%B1%E7%8A%B6%E5%9B%BE%E4%B8%AD%E7%9A%84%E6%9C%80%E5%A4%A7%E7%9F%A9%E5%BD%A2/","title":"单调栈-柱状图中的最大矩形"},{"content":"\n代码求解 利用严格的大压小单调栈，找到每个位置数字左右两边离它最近且比它小的数字，确定以该数字为最小值的子数组的范围，通过计算开头和结尾的可能性确定字数租的个数，乘以该数字得到这部分子数组的最小值之和，每个数在弹出的时候都需要计算答案。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution { public static int MAXN = 30001; public static int MOD = 1000000007; public static int[] stack = new int[MAXN]; public static int r; public static int sumSubarrayMins(int[] arr){ long ans = 0; for(int i=0;i\u0026lt;arr.length;i++){ while(r\u0026gt;0\u0026amp;\u0026amp;arr[stack[r-1]]\u0026gt;=arr[i]){ int cur = stack[--r]; int left = r==0?-1:stack[r-1]; ans = (ans+(long)(cur-left)*(i-cur)*arr[cur])%MOD; } stack[r++]=i; } while(r\u0026gt;0){ int cur = stack[--r]; int left = r==0?-1:stack[r-1]; ans = (ans+(long)(cur-left)*(arr.length-cur)*arr[cur])%MOD; } return (int)ans; } } ","date":"2025-12-29T22:27:43Z","permalink":"https://iamxurulin.github.io/p/%E5%8D%95%E8%B0%83%E6%A0%88-%E5%AD%90%E6%95%B0%E7%BB%84%E7%9A%84%E6%9C%80%E5%B0%8F%E5%80%BC%E4%B9%8B%E5%92%8C/","title":"单调栈-子数组的最小值之和"},{"content":"\n代码求解 求更高温度出现在几天后，这是一个严格大于的问题，需要使用小压大的单调栈。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution { public static int MAXN = 100001; public static int[] stack = new int[MAXN]; public static int r; public static int[] dailyTemperatures(int[] nums){ int n = nums.length; int[] ans = new int[n]; r = 0; for(int i=0,cur;i\u0026lt;n;i++){ while (r\u0026gt;0\u0026amp;\u0026amp;nums[stack[r-1]]\u0026lt;nums[i]) { cur = stack[--r]; ans[cur]=i-cur; } stack[r++]=i; } return ans; } } ","date":"2025-12-29T21:31:19Z","permalink":"https://iamxurulin.github.io/p/%E5%8D%95%E8%B0%83%E6%A0%88-leetcode739%E6%AF%8F%E6%97%A5%E6%B8%A9%E5%BA%A6/","title":"单调栈-Leetcode739每日温度"},{"content":"\n求解代码 main 方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public static int MAXN = 1000001; public static int[] arr = new int[MAXN]; public static int[] stack = new int[MAXN]; public static int[][] ans = new int[MAXN][2]; public static int n,r; public static void main(String[] args)throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); while(in.nextToken()!=StreamTokenizer.TT_EOF){ n = (int)in.nval; for(int i=0;i\u0026lt;n;i++){ in.nextToken(); arr[i]=(int)in.nval; } compute(); for(int i=0;i\u0026lt;n;i++){ out.println(ans[i][0]+ \u0026#34; \u0026#34; + ans[i][1]); } } out.flush(); out.close(); br.close(); } compute 方法 整体来看就是三步走，先遍历，后清算，再修正。\n用数组实现栈，变量r表示栈的使用情况，栈中存放的是数组的下标。\nr=0时，即栈为空时，当前的遍历位置i直接入栈。\n依次比较栈顶以及栈顶以下的元素对应的值和arr[i]的大小，如果大于等于arr[i]。\n出栈后，如果栈为空，则左侧答案为-1，非空的话，左侧答案就是栈顶压着的那个元素；右侧答案就是让其出站的i位置。\n如果遍历结束后，栈中还有元素，还需要依次弹出，也就是清算。\n弹出元素的左侧答案，如果栈为空，则左侧答案为-1，非空的话，左侧答案就是栈顶压着的那个元素；右侧答案一律为-1。\n考虑到存在相等的值，因此，还需要进行修正。\n因为n-1位置右侧的答案一定是-1，所以无需修正。\n如果右侧的答案不是-1，并且答案位置对应的值和当前位置对应的值相等，则将答案位置的右侧答案作为当前位置的右侧答案，这里可能有点拗口，但是仔细想想应该可以理解。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static void compute(){ r = 0; int cur; for(int i = 0;i\u0026lt;n;i++){ while(r\u0026gt;0\u0026amp;\u0026amp;arr[stack[r-1]]\u0026gt;=arr[i]){ cur = stack[--r]; ans[cur][0]=r\u0026gt;0?stack[r-1]:-1; ans[cur][1]=i; } stack[r++]=i; } while(r\u0026gt;0){ cur = stack[--r]; ans[cur][0]=r\u0026gt;0?stack[r-1]:-1; ans[cur][1]=-1; } for(int i=n-2;i\u0026gt;=0;i--){ if(ans[i][1]!=-1\u0026amp;\u0026amp;arr[ans[i][1]]==arr[i]){ ans[i][1]=ans[ans[i][1]][1]; } } } ","date":"2025-12-29T20:57:49Z","permalink":"https://iamxurulin.github.io/p/%E5%8D%95%E8%B0%83%E6%A0%88%E7%BB%93%E6%9E%84%E8%BF%9B%E9%98%B6%E9%A2%98/","title":"单调栈结构进阶题"},{"content":"在Linux系统中，硬链接和软链接都是用于将一个文件链接到另一个文件。\n主要区别如下：\n1.硬链接是通过在文件系统中创建一个新的目录项指向同一文件的inode的位置来实现的，而软链接是通过在文件系统中创建一个包含指向另一个文件的路径的新文件来实现。\n2.硬链接只能指向同一个文件系统内的文件，并不能跨文件系统创建，而软链接可以跨文件系统创建，并且可以指向任意类型的文件。\n3.因为硬链接实际上指向的是同一inode，所以如果原文件被删除了，硬链接还是能够访问到原文件的内容；但是对于软链接来说，如果原文件被删除了，软链接也会跟着失效。\n4.因为硬链接只是增加了一个新的目录项，所以对磁盘空间的消耗比软链接要更小。\n","date":"2025-12-29T17:04:55Z","permalink":"https://iamxurulin.github.io/p/linux%E7%B3%BB%E7%BB%9F%E7%9A%84%E7%A1%AC%E9%93%BE%E6%8E%A5%E5%92%8C%E8%BD%AF%E9%93%BE%E6%8E%A5%E6%9C%89%E7%94%9A%E5%8C%BA%E5%88%AB/","title":"Linux系统的硬链接和软链接有甚区别"},{"content":"循环依赖是指两个或者多个bean之间相互引用，形成了一个闭环。\n最典型的场景就是：\n比如Spring正在创建Bean A，发现它依赖B，于是就去创建B，结果在创建B的时候，又发现它依赖A。\n但是这个时候A正在创建中，还没有完全生成，这样B就拿不到A的引用，所以该咋办呢？\n这里又分为构造器注入和Setter注入两种情况：\n如果是构造器注入，那好办，Spring直接就给你抛出BeanCurrentlyInCreationException错误。\n如果是Setter注入，Spring则是通过三级缓存机制来解决。\n","date":"2025-12-29T16:48:25Z","permalink":"https://iamxurulin.github.io/p/%E4%BB%80%E4%B9%88%E6%98%AF%E5%BE%AA%E7%8E%AF%E4%BE%9D%E8%B5%96/","title":"什么是循环依赖"},{"content":"Kafka索引设计的亮点主要有以下4个方面：\n1.kafka通过稀疏索引只存储每隔一定间隔的消息位置，而不是对每条消息都建立索引，这样可以大幅度减少内存的占用。\n2.Kafka通过将日志文件拆分成多个段文件来存储，每个段文件中包含一个日志文件和对应的索引文件。\n3.kafka为了降低磁盘的随机写入成本，采用顺序写入文件的方式，提升了写入性能。\n4.Kafka为了避免逐个扫描，通过offset定位消息，再结合稀疏索引可以快速定位到近似位置后再进行顺序查找，从而保证高效地读取。\n","date":"2025-12-29T16:31:00Z","permalink":"https://iamxurulin.github.io/p/kafka%E7%9A%84%E7%B4%A2%E5%BC%95%E8%AE%BE%E8%AE%A1%E6%9C%89%E4%BB%80%E4%B9%88%E4%BA%AE%E7%82%B9/","title":"Kafka的索引设计有什么亮点"},{"content":"Kafka中的时间轮是一种用于实现定时清理、消息过期、心跳超时等功能的定时任务调度机制。\n通过时间轮可以避免在大量的定时任务中进行逐个扫描，可以实现接近O(1)的延迟任务管理，降低了内存和CPU的消耗，比较适合管理大量短时和中长期的定时任务。\n时间轮，它可以看成是一个环形数组，其中，每个槽表示一个时间间隔。\nKafka会在时间轮的每个槽中根据任务的到期时间放置延迟任务，当时间轮转到任务所在的槽时，就执行该槽内的任务。\n如果存在到期未完成的任务，则会被重新分配到下一轮。\n","date":"2025-12-29T15:49:00Z","permalink":"https://iamxurulin.github.io/p/kafka%E4%B8%AD%E7%9A%84%E6%97%B6%E9%97%B4%E8%BD%AE%E5%AE%9E%E7%8E%B0/","title":"kafka中的时间轮实现"},{"content":"Kafka的事务消息实现了消息在生产、传输和消费的过程中的“仅一次”传递，也就是所谓的Exactly Once语义。\nKafka的事务消息主要由事务协调器、幂等生产者和事务性消费这三个核心组件来实现。\n事务协调器主要负责事务的启动、提交和终止管理，将事务状态记录到__transaction_state内部的主题；\n为了确保同一事务的每条消息只写入一次，Kafka Producer通过Producer ID来作为事务的唯一性标志。\n在消费的过程中，消费者可以选择只消费已提交的事务消息，从而确保数据的最终一致性。\n我们可以理一下Kafka的事务消息流程：\n首先是生产者向Transaction Coordinator请求启动事务；\n接下来生产者为了保证幂等性，给每条消息都带上唯一的Producer ID和Sequence Number，开始向Kafka写入事务消息；\n当所有消息都写入完成后，生产者开始向事务协调去发送commit或者abort请求提交或者终止事务。\n最后，消费者为了实现最终的数据一致性，可以通过设置read_commited 隔离级别，只消费已提交的消息。\n","date":"2025-12-29T15:06:48Z","permalink":"https://iamxurulin.github.io/p/kafka%E4%B8%AD%E5%85%B3%E4%BA%8E%E4%BA%8B%E5%8A%A1%E6%B6%88%E6%81%AF%E7%9A%84%E5%AE%9E%E7%8E%B0/","title":"Kafka中关于事务消息的实现"},{"content":" 求解代码 maxRunTime方法 假设所有电池的最大电量是max，如果此时sum\u0026gt;(long)max*num，那么最终的供电时间一定会大于等于max，由此也能推出最终的答案为sum/num。\n对于sum\u0026lt;=(long)max*num的情况，在0~max区间内不断二分查找即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static long maxRunTime(int num,int[] arr){ int max = 0; long sum = 0; for(int x:arr){ max = Math.max(max,x); sum+=x; } if(sum\u0026gt;(long)max*num){ return sum/num; } int ans = 0; for(int l=0,r=max,m;l\u0026lt;=r;){ m= l+((r-l)\u0026gt;\u0026gt;1); if(f(arr,num,m)){ ans = m; l = m+1; }else{ r=m-1; } } return ans; } f方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static boolean f(int[] arr, int num, int time) { long sum = 0; for (int x : arr) { if (x \u0026gt; time) { num--; } else { sum += x; } if (sum \u0026gt;= (long) num * time) { return true; } } return false; } ","date":"2025-12-28T19:56:01Z","permalink":"https://iamxurulin.github.io/p/%E5%90%8C%E6%97%B6%E8%BF%90%E8%A1%8Cn%E5%8F%B0%E7%94%B5%E8%84%91%E7%9A%84%E6%9C%80%E9%95%BF%E6%97%B6%E9%97%B4/","title":"同时运行N台电脑的最长时间"},{"content":"在用WebStrorm2023调试前端代码时出现以下报错：\n1 2 3 ERROR in ./node_modules/monaco-editor/esm/vs/base/browser/browser.js Module build failed (from ./node_modules/babel-loader/lib/index.js): SyntaxError: ..\\node_modules\\monaco-editor\\esm\\vs\\base\\browser\\browser.js: Static class blocks are not enabled. Please add @babel/plugin-transform-class-static-block to your configuration. 这个错误是因为 monaco-editor 使用了静态类块（static class blocks）语法，但 Babel 配置中没有相应的插件来转换它。\n接下来采取以下步骤可以解决这个bug：\n1.首先需要安装 Babel 插件 在webstorm的Terminal执行以下命令：\n1 npm install --save-dev @babel/plugin-transform-class-static-block 2.修改 babel.config.js 1 2 3 4 5 6 7 8 module.exports = { presets: [ \u0026#39;@vue/cli-plugin-babel/preset\u0026#39; ], plugins: [ \u0026#39;@babel/plugin-transform-class-static-block\u0026#39; ] } 3.修改 vue.config.js 1 2 3 4 5 6 7 8 9 10 11 const { defineConfig } = require(\u0026#34;@vue/cli-service\u0026#34;); const MonacoWebpackPlugin = require(\u0026#34;monaco-editor-webpack-plugin\u0026#34;); module.exports = defineConfig({ transpileDependencies: true, chainWebpack(config) { config.plugin(\u0026#34;monaco\u0026#34;).use(new MonacoWebpackPlugin({ languages: [\u0026#39;javascript\u0026#39;, \u0026#39;typescript\u0026#39;, \u0026#39;json\u0026#39;, \u0026#39;html\u0026#39;, \u0026#39;css\u0026#39;, \u0026#39;cpp\u0026#39;, \u0026#39;java\u0026#39;, \u0026#39;python\u0026#39;] })); }, }); 4.清理缓存并重启 1 2 3 4 5 # 删除缓存 Remove-Item -Recurse -Force node_modules\\.cache -ErrorAction SilentlyContinue # 重新运行 npm run serve ⚠️注意： 在WebStorm中开的Terminal是PowerShell形式的，格式跟普通的Terminal有差异。\n","date":"2025-12-28T18:05:20Z","permalink":"https://iamxurulin.github.io/p/error-in-.-node_modules-monaco-editor-esm-vs-base-browser-browser.js-module-build-failed%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95/","title":"ERROR-in-.-node_modules-monaco-editor-esm-vs-base-browser-browser.js-Module-build-failed解决办法。"},{"content":"RockMQ事务消息的缺点主要就以下几个方面：\n从改造成本来看，RocketMQ需要改造它的原始逻辑来实现一个特定的接口，并且还需要在应用层来处理一个复杂的回查逻辑，从而确保回查不会重复或者丢失。\n在可用性方面，由于RocketMQ的事务消息的实现是先发送半消息，如果MQ集群挂了，那么半消息就没法发送成功，后续的逻辑就无法再执行下去了，也就是说整个应用无法正常执行了。\n还有一个缺点就是RocketMQ只支持单事务消息。\n","date":"2025-12-28T17:01:10Z","permalink":"https://iamxurulin.github.io/p/rocketmq%E7%9A%84%E4%BA%8B%E5%8A%A1%E6%B6%88%E6%81%AF%E6%9C%89%E4%BB%80%E4%B9%88%E7%BC%BA%E7%82%B9%E4%BD%A0%E7%9F%A5%E9%81%93%E5%90%97/","title":"RocketMQ的事务消息有什么缺点你知道吗"},{"content":"推模式是消息队列主动将消息从Broker推送给Consumer，这种模式它的实时性比较好，消息可以立即送达消费者，但是缺点就是决定权不在消费者手上，在高并发场景下容易造成消费者过载。\n拉模式是消费者主动从消息队列中的Broker拉取消息，这种模式就是根据消费者自身的负载和消费能力来决定拉去消息的频率，这样就可以避免过载，但是这种模式的实时性比不上推模式，而且还会导致消息延迟。\nRocketMQ和Kafka都选择了拉模式，对于拉模式无法保证实时性的缺点，他们采取的是长轮询的方式来解决。\n主要是通过在消费者去Broker拉取消息时，当有消息存在的话，那自然就直接返回消息；当没有消息时，则保持连接，暂时hold住请求，之后如果在对应的队列或者分区有新的消息到来时就通过之前hold住的请求及时地返回消息。\n","date":"2025-12-28T16:28:14Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E7%9A%84%E6%8E%A8%E6%A8%A1%E5%BC%8F%E5%92%8C%E6%8B%89%E6%A8%A1%E5%BC%8F/","title":"说说消息队列的推模式和拉模式"},{"content":"在消息队列中，当消息的生产速度远远大于消费速度时，将会导致大量的消息积压在队列中，这样就会形成消息堆积。\n那如何处理消息堆积呢？\n一般来说，我们需要定位一下消费慢的原因，如果是bug则处理bug； 如果是因为消费者自身的消费能力弱，则需要考虑提升消费者的消费能力。\n那怎么提高消费者的消费能力呢？\n1.可以考虑增加消费者的线程数量，提高消费者的并发消费能力；\n2.如果是在分布式系统中，可以增加多个消费实例，从而提高消费速率。\n3.优化消费者的代码，比如可以减少I/O操作或者使用批量处理等等。\n上面这些是通过消费者端进行解决的方法，如果是从生产端解决的话，可以考虑对生产端进行限流，降低生产速率。\n","date":"2025-12-28T15:14:44Z","permalink":"https://iamxurulin.github.io/p/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86%E6%B6%88%E6%81%AF%E5%A0%86%E7%A7%AF/","title":"消息队列如何处理消息堆积"},{"content":"\n求解代码 这道题面试遇到过，是一道比较经典的动态规划题。\n先求出word1和word2的长度，然后把初始条件和状态转移方程写出来，基本上这题就完成了。\n初始条件：\n当j为0时，执行删除操作；\n当i为0时，执行插入操作。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public int minDistance(String word1,String word2){ int m = word1.length(); int n = word2.length(); int[][] dp = new int[m+1][n+1]; for(int i=0;i\u0026lt;=m;i++){ dp[i][0]=i; } for(int j=0;j\u0026lt;=n;j++){ dp[0][j]=j; } for(int i=1;i\u0026lt;=m;i++){ for(int j=1;j\u0026lt;=n;j++){ if(word1.charAt(i-1)==word2.charAt(j-1)){ dp[i][j]=dp[i-1][j-1]; }else{ dp[i][j]=Math.min(Math.min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1])+1; } } } return dp[m][n]; } ","date":"2025-12-27T21:13:21Z","permalink":"https://iamxurulin.github.io/p/%E7%BC%96%E8%BE%91%E8%B7%9D%E7%A6%BB-%E6%89%8B%E6%92%95/","title":"编辑距离-手撕"},{"content":"\n代码求解 smallestDistancePair方法 先排序，用最大值减去最小值求出最大的数对距离。\n通过二分法调用f函数找出区间内满足要求的数对，然后不停地二分。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static int smallestDistancePair(int[] nums,int k){ int n = nums.length; Arrays.sort(nums); int ans = 0; for(int l=0,r=nums[n-1]-nums[0],m,cnt;l\u0026lt;=r;){ m = l+((r-l)\u0026gt;\u0026gt;1); cnt = f(nums,m); if(cnt\u0026gt;=k){ ans = m; r = m-1; }else{ l=m+1; } } return ans; } f方法 求arr中数对距离小于limit的数对总共有几对。\n1 2 3 4 5 6 7 8 9 10 public static int f(int[] arr,int limit){ int ans = 0; for(int l=0,r=0;l\u0026lt;arr.length;l++){ while(r+1\u0026lt;arr.length \u0026amp;\u0026amp; arr[r+1]-arr[l]\u0026lt;=limit){ r++; } ans+=r-l; } return ans; } ","date":"2025-12-27T20:10:12Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%88%86%E6%B3%95%E6%89%BE%E5%87%BA%E7%AC%ACk%E5%B0%8F%E7%9A%84%E6%95%B0%E5%AF%B9%E8%B7%9D%E7%A6%BB/","title":"二分法找出第k小的数对距离"},{"content":" 求解代码 Main方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static int MAXN = 100001; public static int[] arr = new int[MAXN]; public static int n; public static void main(String[] args)throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); while(in.nextToken()!=StreamTokenizer.TT_EOF){ n = (int)in.nval; int l = 0; int r = 0; for(int i=1;i\u0026lt;=n;i++){ in.nextToken(); arr[i]=(int)in.nval; r=Math.max(r,arr[i]); } out.println(compute(l,r,r)); } out.flush(); out.close(); br.close(); } compute方法 区间[l,r]表示通关所需要的最小的能量范围， max表示所有建筑的最大高度。\n1 2 3 4 5 6 7 8 9 10 11 12 13 public static int compute(int l,int r,int max){ int m,ans = -1; while(l\u0026lt;=r){ m = l+((r-l)\u0026gt;\u0026gt;1); if(f(m,max)){ ans = m; r=m-1; }else{ l=m+1; } } return ans; } f方法 初始能量设置为energy，max是建筑的最大高度，这里有个需要注意的点就是：\n初始能量越小，它就越吃亏，遇到比它高的建筑，它减小的更快；遇到比它低的建筑，增加的也更慢。\n一旦能量超过了建筑高度的最大值，肯定可以通关，可以直接返回。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static boolean f(int energy,int max){ for(int i=1;i\u0026lt;=n;i++){ if(energy\u0026lt;=arr[i]){ energy-=arr[i]-energy; }else{ energy+=energy-arr[i]; } if(energy\u0026gt;=max){ return true; } if(energy\u0026lt;0){ return false; } } return true; } ","date":"2025-12-27T19:21:54Z","permalink":"https://iamxurulin.github.io/p/%E6%9C%BA%E5%99%A8%E4%BA%BA%E8%B7%B3%E8%B7%83%E9%97%AE%E9%A2%98/","title":"机器人跳跃问题"},{"content":"为了保证消息的有效性，可采取如下方法：\n1.为了确保消息按照生产者的发送顺序来消费，可以使用单个生产者向单个队列发送消息，再由单个消费者来处理消息。\n2.对于像Kafka和RocketMQ这种支持分区的消息队列，可以通过Partition Key将消息发送到一个特定的分区。因为每个分区的内部都是有序的，这样一来就能保证具有相同Partition Key的消息都按照顺序来消费。\n3.对于像RabbitMQ这种支持顺序队列的消息队列，因为消息在队列中的存储顺序和投递顺序都是一致的，如果使用单个顺序队列，那么消息也将按照顺序被消费。\n","date":"2025-12-27T17:56:38Z","permalink":"https://iamxurulin.github.io/p/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81%E6%B6%88%E6%81%AF%E7%9A%84%E6%9C%89%E6%95%88%E6%80%A7/","title":"消息队列如何保证消息的有效性"},{"content":"对于平常的业务而言，消息重复一般是不可避免的，我们只能在业务上来处理重复消息所带来的影响。\n为了保证同一条消息无论被消费多少次，它的结果都是一样的，需要让消费者的处理逻辑具有幂等性。\n为了实现幂等处理重复消息，可以给每条消息分配一个全局的唯一ID，当消费者接收消息时，先检查一下这个ID是否已经处理，如果已经处理则直接返回，如果没有处理，则执行逻辑并记录一下ID。\n除了分配全局唯一ID，对于订单数据来说，还可以设计一个严格的状态流转（比如：待支付→已支付→待发货→已发货），这样在每次处理消息前对当前状态进行检查，看是否符合操作条件。\n","date":"2025-12-27T17:03:04Z","permalink":"https://iamxurulin.github.io/p/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86%E9%87%8D%E5%A4%8D%E6%B6%88%E6%81%AF/","title":"消息队列如何处理重复消息"},{"content":"为了保证消息不丢失，需要生产消息、存储消息和消费消息这三个阶段协同配合。\n当生产者在发送消息时，需要通过消息确认机制来确保消息的成功到达。\n我们知道存储在内存中的消息是断电不保存的，因此，为了避免消息因为服务器的宕机或者重启而丢失，需要在Broker收到消息后，将其持久化到磁盘中。\n在消费者处理完消息后，需要向消息队列发送确认，如果没有收到消费者发送过来的确认，消息队列则需要重新投递该消息。\n","date":"2025-12-27T16:15:36Z","permalink":"https://iamxurulin.github.io/p/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81%E6%B6%88%E6%81%AF%E4%B8%8D%E4%B8%A2%E5%A4%B1/","title":"消息队列如何保证消息不丢失"},{"content":"常见的消息队列模型主要有发布/订阅模型和队列模型（也称点对点模型）两种。\n那什么是队列模型呢？所谓的队列模型，指的是消息从生产者发送到队列中，其中的消息只能被一个消费者消费一次，在消费者消费完之后，消息就在队列中被删除了。\n而发布/订阅模型，则指的是生产者将消息发布到某个Topic中，这样所有订阅了这个主题的消费者都可以接收到这个消息，这种模型比较适用于像广播通知和实时推送这样的场景。\n需要说明的是，RabbitMQ虽然具有发布/订阅模式，但是在本质上RabbitMQ还是通过同时将消息发送给多个队列来模拟出发布/订阅的效果，其底层依然是基于队列模型的。\n而RocketMQ和Kafka则都是采用发布/订阅模型的。\n","date":"2025-12-27T15:36:30Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E4%B8%80%E4%B8%8B%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E6%9C%89%E5%93%AA%E4%BA%9B%E6%A8%A1%E5%9E%8B/","title":"说一下消息队列有哪些模型"},{"content":"消息队列是一种用于在分布式系统中，解耦发送方和接收方之间通信的异步通信机制。\n消息队列通过引入一个broker作为中间缓冲区，然后将消息存储在broker中，接下来再由消费者从broker中读取和处理消息，从而实现异步通信。\n消息队列常见的用途主要有：\n1.生产者可以在发送消息之后立即返回，而消费者也可以在恰当的时机对消息进行处理。\n2.在高并发的场景下，消息队列可以暂时存储大量的请求，平滑高峰期的流量，不至于使系统过载。\n3.为了减少用户请求的响应时间，可以选择将不需要立即处理的任务放入消息队列中异步执行。\n","date":"2025-12-27T14:42:56Z","permalink":"https://iamxurulin.github.io/p/%E8%A7%A3%E9%87%8A%E4%B8%80%E4%B8%8B%E4%BB%80%E4%B9%88%E6%98%AF%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/","title":"解释一下什么是消息队列"},{"content":"在RocketMQ中，为了确保消息与本地事务的一致性，生产者会先将消息发送到RocketMQ的Topic中，这个时候消息的状态为半消息，对于消费者来说，还是不可见的。\n接下来，生产者会执行本地事务：\n如果本地事务成功了，生产者就会向RocketMQ执行commit操作，将半消息转变为正式消息，这样消费者就可见了。\n如果本地事务失败了，生产者就会向RocketMQ执行Rollback操作，然后RocketMQ就会丢弃这个半消息。\n如果生产者没有及时地执行commit或者rollback操作，那么RocketMQ就会定时地回查本地事务的状态，从而来决定是否commit或者rollback消息。\n","date":"2025-12-26T16:46:06Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4rocketmq%E4%B8%AD%E5%85%B3%E4%BA%8E%E4%BA%8B%E5%8A%A1%E6%B6%88%E6%81%AF%E7%9A%84%E5%AE%9E%E7%8E%B0/","title":"说说RocketMQ中关于事务消息的实现"},{"content":"宏观层面上说：\n面向对象是一种以对象为中心，以人类的思维方式来编写代码的编程风格，\n而面向过程则是一种以过程或者说函数为中心，以计算机的思维方式来写代码的编程风格。\n主要有以下几点区别：\n面向对象主要关注的是对象之间的关系和交互，而面向过程主要关注函数的执行步骤和顺序。 面向对象的数据和行为都是封装在对象内部的，而面向过程的数据和函数却是分离的。 面向对象通过它的封装、继承和多态机制 具有较高的复用性和可拓展性，而面向过程如果进行扩展可能需要修改已有的代码，复用性往往较低。 从适用场景上来说，面向对象很适合一些复杂系统的开发，而面向过程则只适用于一些简单的顺序性较强的小型项目的开发。 ","date":"2025-12-26T15:11:26Z","permalink":"https://iamxurulin.github.io/p/java%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A8%8B%E5%92%8C%E9%9D%A2%E5%90%91%E8%BF%87%E7%A8%8B%E7%BC%96%E7%A8%8B%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88/","title":"Java面向对象编程和面向过程编程的区别是什么"},{"content":" 看看这张图，B和C都继承了A，D又继承了B和C。\n想象这样一种场景，如果此时需要调用D中的一个定义在A的方法，但是呢，这个方法在B和C中存在不同的实现，这个时候就会出现歧义，从而无法抉择应该调用哪个。\n","date":"2025-12-26T14:20:22Z","permalink":"https://iamxurulin.github.io/p/%E4%BD%A0%E7%9F%A5%E9%81%93%E4%B8%BA%E4%BB%80%E4%B9%88java%E4%B8%8D%E6%94%AF%E6%8C%81%E5%A4%9A%E9%87%8D%E7%BB%A7%E6%89%BF%E5%90%97/","title":"你知道为什么Java不支持多重继承吗"},{"content":"Java中的不可变类指的是在对象完成创建之后无法被修改的类。\n最典型的就是String，当然还有所有的基本类型的包装类，比如Integer、Long、Boolean等等。\n如果要实现一个不可变类，至少需要同时满足以下5个条件：\n为了防止子类继承父类之后添加了一些可变行为，导致不可变性遭到破坏，需要将类声明为final。\n为了保证字段只能在构造阶段赋值，并且之后就不能通过外部进行访问或者修改，需要通过private final对字段进行修饰，将变量锁死。\n需要通过构造函数一次性初始化所有字段。\n可以提供getter方法，但是不能提供setter或者修改状态的方法。\n如果类中含有可变对象的引用，需要确保在对象外部无法对这些引用进行修改。\n这样一个不可变类，因为不能对它进行修改，所以多线程访问时不需要加锁，天生的线程安全。\n又因为它的状态不可变，所以很适合作为缓存的key。\n不过，实现这种不可变的代价就是比较费内存。\n","date":"2025-12-25T17:47:54Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4java%E4%B8%AD%E7%9A%84%E4%B8%8D%E5%8F%AF%E5%8F%98%E7%B1%BB/","title":"说说Java中的不可变类。"},{"content":"其实啊，在Java中，不管是基本数据类型还是引用数据类型，参数传递的方式都只有按值传递这一种。\n对于基本数据类型，也就是（ byte, short, int, double, long, float, boolean, char）这8种，这些都是存储在栈内存中的。作为参数传递时，它们传递的都是值的副本，也就是基本数据类型的数值本身。\n对方法中的参数进行操作时，并不会影响原始的变量。\n对于引用数据类型，也就是所有的对象和数组，它们存储的是对象在堆内存中的地址，作为参数传递时，传递的是引用的副本。\n方法内的修改可以通过引用影响到传入的对象的内容，但是并不会影响对象引用本身的地址。\n","date":"2025-12-25T16:57:10Z","permalink":"https://iamxurulin.github.io/p/%E5%9C%A8java%E4%B8%AD%E5%8F%82%E6%95%B0%E4%BC%A0%E9%80%92%E7%9A%84%E6%96%B9%E5%BC%8F%E6%98%AF%E6%8C%89%E5%80%BC%E8%BF%98%E6%98%AF%E6%8C%89%E5%BC%95%E7%94%A8/","title":"在Java中参数传递的方式是按值还是按引用"},{"content":"前端初始化的时候出现这样一个错误：\n1 2 3 4 5 6 ERROR in src/router/index.ts:1:64 TS7016: Could not find a declaration file for module \u0026#39;vue-router\u0026#39;. \u0026#39;./node_modules/vue-router/index.js\u0026#39; implicitly has an \u0026#39;any\u0026#39; type. Try npm i --save-dev @types/vue-router if it exists or add a new declaration (.d.ts) file containing declare module \u0026#39;vue-router\u0026#39;; \u0026gt; 1 | import { createRouter, createWebHistory, RouteRecordRaw } from \u0026#34;vue-router\u0026#34;; | ^^^^^^^^^^^^ 2 | import HomeView from \u0026#34;../views/HomeView.vue\u0026#34;; 我尝试了以下方法：\n1 2 3 4 5 6 7 8 9 # 使用 PowerShell Remove-Item -Recurse -Force node_modules Remove-Item package-lock.json # 清理 npm 缓存 npm cache clean --force # 重新安装 npm install 重新运行package.jason文件的serve仍然不行；\n再次尝试新建 src/shims-vue-router.d.ts文件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 declare module \u0026#39;vue-router\u0026#39; { import type { App, Component } from \u0026#39;vue\u0026#39; export interface RouteRecordRaw { path: string name?: string | symbol component?: Component components?: Record\u0026lt;string, Component\u0026gt; redirect?: string alias?: string | string[] children?: RouteRecordRaw[] meta?: Record\u0026lt;string, any\u0026gt; beforeEnter?: NavigationGuard | NavigationGuard[] props?: boolean | Record\u0026lt;string, any\u0026gt; | ((to: any) =\u0026gt; Record\u0026lt;string, any\u0026gt;) } export interface Router { currentRoute: any push(to: any): Promise\u0026lt;any\u0026gt; replace(to: any): Promise\u0026lt;any\u0026gt; go(delta: number): void back(): void forward(): void beforeEach(guard: NavigationGuard): () =\u0026gt; void afterEach(guard: NavigationHookAfter): () =\u0026gt; void install(app: App): void // 添加这行,让 Router 可以作为 Vue 插件使用 } export interface NavigationGuard { (to: any, from: any, next: any): any } export interface NavigationHookAfter { (to: any, from: any): any } export function createRouter(options: any): Router export function createWebHistory(base?: string): any export function createWebHashHistory(base?: string): any export function createMemoryHistory(base?: string): any export function useRouter(): Router export function useRoute(): any } 这下可以了：\n","date":"2025-12-25T15:45:44Z","permalink":"https://iamxurulin.github.io/p/ts7016-could-not-find-a-declaration-file-for-module-vue-router.%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95/","title":"TS7016-Could-not-find-a-declaration-file-for-module-‘vue-router‘.解决办法"},{"content":"我认为Java的优势主要有以下几点：\n1.因为有JVM这个充当翻译官的角色存在，程序员编写的代码编译成字节码后，不管是在Windows、Linux还是Mac系统上，JVM都可以把它翻译成机器可以读懂的语言，也就是可以实现跨平台，在企业的开发中，便于部署。\n2.与C++需要手动的申请以及释放内存不同的是，Java自带了垃圾回收机制（GC），可以实现自动在后台帮程序员打扫内存垃圾，这样的话程序员的精力可以更多的倾向于业务逻辑。\n3.Java的生态圈十分的庞大，可以说不管你想做什么功能，基本上你都能找到像Spring全家桶那样现成的并且是成熟的开源库，这样你就不用重复的去造轮子。\n4.与面向过程的编程语言不同，Java是一种面向对象的编程语言，具备封装、继承和多态这3个特性。这种面向对象的特性，使得企业在开发和维护那种比较大规模、并且复杂度较高的系统时会相对容易一些。\n","date":"2025-12-25T13:12:33Z","permalink":"https://iamxurulin.github.io/p/%E8%AF%B4%E8%AF%B4%E7%9C%8B%E4%BD%A0%E8%AE%A4%E4%B8%BAjava%E7%9A%84%E4%BC%98%E5%8A%BF%E6%98%AF%E4%BB%80%E4%B9%88/","title":"说说看你认为Java的优势是什么"},{"content":"Java的多态指的是父类的引用指向子类的对象，使得在调用同一个方法时，会执行子类自己的实现。\n通常来说，多态有3个必要条件。\n1.要有继承关系，比如子类继承父类，或者实现关系，比如类实现接口；\n2.子类需要重写父类的方法；\n3.父类的引用指向子类的对象，换句话说就是把子类类型的对象赋值给父类类型的引用变量，也就是向上转型。\n在面试的时候，有些面试官可能会问你多态的作用，多态的作用就两个：\n一个是解耦，也就是调用方只依赖父类或者接口，并不会依赖它的具体实现。\n另一个是可拓展，就是说可以在完全不修改原有代码的情况下新增一个子类。\n多态性的实现方式 在面向对象的编程中，多态性有编译时多态和运行时多态两种实现方式。\n编译时多态也称作静态多态，通过方法重载来实现，Java编译器会在编译时根据方法调用时传入的参数类型和数量来决定具体调用哪一个重载方法。\n运行时多态也称作动态多态，通过方法重写来实现，当父类的引用调用方法时，在运行时会根据对象的实际类型来决定实际执行的子类重写之后的方法。\n其中，\n方法重载指的是同一个类的多个方法，它们的名称相同但是参数列表不同。\n方法重写指的是子类重写了父类的一个或者多个方法。\n","date":"2025-12-24T18:25:20Z","permalink":"https://iamxurulin.github.io/p/%E4%BB%80%E4%B9%88%E6%98%AFjava%E7%9A%84%E5%A4%9A%E6%80%81%E7%89%B9%E6%80%A7/","title":"什么是Java的多态特性"},{"content":"在Java中，Exception和Error两者都是Throwable类的子类。\n总体上来说，Exception是指可以被处理的程序异常，属于业务逻辑的范畴，常见的异常有NullPointerException和IOException等等；\n而Error则指的是系统级的不可恢复的错误，错误出现以后一般就意味着进程需要终止或者重启，常见的错误有OutOfMemoryError、StackOverflowError等等。\n如果细分的话，Exception又可以分为Checked Exception和Unchecked Exception，也就是受检异常和非受检异常。\n常见的受检异常有IOException和SQLException，它们都是从Exception继承过来但是并不继承RuntimeException，在编译的时候必须使用try-catch块或者throws声明进行抛出这样的显式处理。\n而非受检异常如NullPointerException和IndexOutOfBoundsException都是继承自RuntimeException，它们就不需要进行显式的处理，而是运行的时候才会抛出。\n异常处理时需要注意的点： 1.如果什么异常都使用通用Exception，而不是特定的异常，那么其他同事在分析这段代码时就无法一眼看出这段代码实际想要捕获的异常。\n所以，尽量不要捕获类似于Exception这样的通用异常，而应该捕获特定的异常。\n2.如果我们在捕获异常之后不把异常进行抛出或者使用catch之后再用e.printStackTrace()输出一个标准错误流，这样操作的话也许这只是一个非常简单的bug，但是你仍然不知道是哪里出错了以及出错的原因。\n所以，我们可以按需自定义一定的格式，将详细的信息输入到日志系统中，这样的话便于后续进行错误的排查。\n3.想象这样一种场景，如果一个方法，这个方法内部调用了其他好几个方法，现在这个最开始的方法的某个参数传入了一个null值，但是，你并没有立刻处理这个情况，而是在这个最开始的方法调用了好几个方法之后才爆出这里其实最开始就有一个空指针。\n这样就会造成其实你明明只需要在最开始只抛出一点点信息就能够定位到这个错误所在的地方，但是当最开始的方法调用了其他好几个方法之后你再抛出的可能就是很多的堆栈信息。\n因此 ，最好不要延迟处理异常。\n4.try-catch中的代码会影响JVM对代码的优化，因此，使用try-catch块时，不推荐try住一大段代码，而是在必要的代码段使用try-catch，也就是说try-catch的范围应该尽量小。\n5.在控制程序的流程这一块，我们一般采取的原则是能用if/else语句来控制的话就不要用异常，直白点说就是不要通过异常来控制程序的流程。\n6.在finally中return或者处理返回值往往会发生覆盖try中的return的情况，因此，最好不要在finallly中处理返回值或者return。\n","date":"2025-12-24T16:50:46Z","permalink":"https://iamxurulin.github.io/p/java%E4%B8%AD%E7%9A%84exception%E5%92%8Cerror%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB/","title":"Java中的Exception和Error有什么区别"},{"content":"先来说说序列化，序列化是把内存中的对象转变成二进制的数据，通过这种形式才能存储到硬盘或者发送给别人。\n再来说说反序列化，由于硬盘和网络都只认识二进制的0和1，不认识Java对象，所以必须进行转换，把二进制的数据变回内存中的对象。\n在具体的实现细节上，有怎么做、怎么防、怎么稳这3个问题面试官比较喜欢问。\n对于怎么做这个问题，一个类要想实现序列化，就必须实现Serializable接口，这个接口就相当于一个许可证。\n至于怎么防，如果有些像密码这种的字段，它不想被保存下来，我们可以通过加上一个transient关键字，这样的话，序列化的时候就会自动跳过这些字段。\n对于怎么稳，为了防止你修改了代码之后，在读取旧数据时发生报错，可以加上一个serialVersionUID版本号，这个版本号就相当于一个文件的身份证或者指纹。\n这个serialVersionUID可以用来验证序列化的对象和反序列化之后对应的对象的ID两者是否一致。\n举个例子说明一下：\n比如，如果在没有定义一个serialVersionUID的前提下，接着序列化一个对象，在反序列化之前，在对象中增加了一个成员变量使得对象的类结构发生了改变，这个时候进行反序列化就会失败。原因就是serial VersionUID已经不一致了。\n","date":"2025-12-24T15:37:56Z","permalink":"https://iamxurulin.github.io/p/java%E4%B8%AD%E7%9A%84%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%98%AF%E4%BB%80%E4%B9%88/","title":"Java中的序列化和反序列化是什么"},{"content":"\n求解思路 这道题与其直接想\u0026quot;怎么分割数组\u0026quot;，不如先问\u0026quot;如果我规定每个子数组的和不能超过某个值 x，那最少需要分成几部分？\u0026quot;\n通过这个反向思考，我们可以用二分搜索来找答案。\n想象一下调节音量的过程，我们在 0 到数组总和之间不断试探一个\u0026quot;限制值\u0026quot;，每次尝试时都检查：\n在这个限制下能否把数组分成不超过 k 个部分。\n如果可以做到，说明这个限制还能再小一点，我们就往更小的方向试；\n如果做不到，说明限制太严格了,我们就往更大的方向试。\n这样一步步逼近,最终就能找到那个\u0026quot;恰好能分成 k 个部分\u0026quot;的最小限制值，这个值就是答案。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public static int splitArray(int[] nums, int k) { // 计算数组总和，这是二分搜索的右边界 long sum = 0; for (int num : nums) { sum += num; } long ans = 0; // 在 [0, sum] 区间进行二分搜索 for (long l = 0, r = sum, m, need; l \u0026lt;= r;) { // 计算中点 m = l + ((r - l) \u0026gt;\u0026gt; 1); // 关键：如果限制每部分和不超过 m，需要分成几个部分? need = f(nums, m); if (need \u0026lt;= k) { // 如果分成的部分数 \u0026lt;= k，说明 m 可行 // 但可能还能更小，继续在左半边找 ans = m; r = m - 1; } else { // 如果需要分成的部分 \u0026gt; k，说明 m 太小了 // 需要增大限制，在右半边找 l = m + 1; } } return (int) ans; } // 辅助函数：给定限制 limit，计算最少需要分成几个部分 public static int f(int[] arr, long limit) { int parts = 1; // 至少需要 1 个部分 int sum = 0; // 当前部分的累加和 for (int num : arr) { // 如果单个元素就超过限制，无法分割 if (num \u0026gt; limit) { return Integer.MAX_VALUE; } // 如果加上当前元素会超过限制 if (sum + num \u0026gt; limit) { parts++; // 开启新的部分 sum = num; // 当前元素作为新部分的起始 } else { sum += num; // 继续累加到当前部分 } } return parts; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-21T19:08:33Z","permalink":"https://iamxurulin.github.io/p/%E8%BF%99%E9%81%93%E9%A2%98%E5%91%8A%E8%AF%89%E4%BD%A0-%E6%9C%89%E6%97%B6%E5%80%99-%E5%8F%8D%E7%9D%80%E6%83%B3-%E5%B0%B1%E5%AF%B9%E4%BA%86/","title":"这道题告诉你-有时候-反着想-就对了"},{"content":"\n求解思路 这道题我们可以把速度的范围确定在1到最大堆的香蕉数量之间,因为速度最小是1(总得吃),最大也不需要超过最大堆的数量(再快也没意义)。\n然后我们在这个范围内不断尝试中间值,对于每个速度计算需要多少小时吃完,\n如果小于等于h小时说明这个速度可行,但我们要找最小的,所以继续往左半边找更小的速度;\n如果超过h小时说明速度太慢了,就往右半边找更快的速度。\n这样不断缩小范围,最终就能找到刚好满足条件的最小速度。\n代码解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public static int minEatingSpeed(int[] piles, int h) { // 确定速度的搜索范围[l,r] int l = 1; // 最小速度为1 int r = 0; for (int pile : piles) { r = Math.max(r, pile); // 最大速度为最大堆的数量 } // 在[l,r]区间内二分查找 int ans = 0; int m = 0; while (l \u0026lt;= r) { m = l + ((r - l) \u0026gt;\u0026gt; 1); // 计算中间速度,避免溢出 if (f(piles, m) \u0026lt;= h) { // 当前速度可以在h小时内吃完 ans = m; // 记录答案 r = m - 1; // 尝试更小的速度 } else { // 当前速度太慢,需要更快 l = m + 1; } } return ans; } // 计算以speed速度吃完所有香蕉需要的小时数 public static long f(int[] piles, int speed) { long ans = 0; for (int pile : piles) { // 向上取整:(pile + speed - 1) / speed // 例如:pile=11,speed=4 → (11+3)/4=3小时 ans += (pile + speed - 1) / speed; } return ans; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-20T18:46:01Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%96%B0%E7%8E%A9%E6%B3%95-%E4%B8%8D%E5%9C%A8%E6%95%B0%E7%BB%84%E9%87%8C%E6%89%BE-%E8%80%8C%E5%9C%A8-%E5%8F%AF%E8%83%BD%E7%9A%84%E7%AD%94%E6%A1%88-%E9%87%8C%E6%89%BE/","title":"二分查找新玩法-不在数组里找-而在-可能的答案-里找"},{"content":"\n求解思路 这道题我们把数组想象成一个特殊的容器,理想情况下索引 0 应该放数字 1,索引 1 应该放数字 2,以此类推。\n我们用两个指针来维护三个区域:\n左边是已经整理好的\u0026quot;完美区\u0026quot;(每个位置都放着正确的数字),\n中间是待处理区,\n右边是\u0026quot;垃圾区\u0026quot;(存放那些不可能成为答案的数字,比如负数、超大的数、重复的数)。\n处理过程就像整理房间,\n我们盯着待处理区的左边界,如果这个位置的数字恰好是它该在的位置,就扩大完美区;\n如果这个数字是垃圾(太小、太大或重复),就把它扔到垃圾区;\n如果这个数字本身没问题但放错了位置,就把它交换到它应该去的地方。\n这样不断循环,当完美区和垃圾区碰头时,完美区的长度加 1 就是我们要找的第一个缺失正数。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static int firstMissingPositive(int[] arr) { int l = 0; // 左指针,左边是完美区 [0...l-1] int r = arr.length; // 右指针,右边是垃圾区 [r...n-1] while (l \u0026lt; r) { if (arr[l] == l + 1) { // 当前位置数字正确,扩大完美区 l++; } else if (arr[l] \u0026lt;= l || arr[l] \u0026gt; r || arr[arr[l] - 1] == arr[l]) { // 垃圾数字:太小、太大或目标位置已有相同数字 swap(arr, l, --r); } else { // 将数字交换到它应该在的位置 swap(arr, l, arr[l] - 1); } } return l + 1; // 完美区长度+1就是答案 } public static void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } 举个栗子 假设输入数组为 [3, 4, -1, 1]:\n初始:l=0, r=4, arr=[3,4,-1,1] arr[0]=3,交换到索引2:arr=[−1,4,3,1] arr[0]=-1是垃圾,扔到垃圾区:arr=[1,4,3,−1], l=0, r=3 arr[0]=1正确,l++:l=1 arr[1]=4\u0026gt;r=3,是垃圾:arr=[1,3,4,−1], l=1, r=2 arr[1]=3\u0026gt;r=2,是垃圾:arr=[1,3,4,−1], l=1, r=1 退出循环,返回 l+1=2 答案是 2,因为数组中有 1 但没有 2。\n如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-19T20:04:55Z","permalink":"https://iamxurulin.github.io/p/%E6%95%B0%E7%BB%84%E8%87%AA%E5%B7%B1%E5%B0%B1%E6%98%AF%E5%93%88%E5%B8%8C%E8%A1%A8-%E8%BF%99%E9%81%93hard%E9%A2%98%E7%9A%84%E8%A7%A3%E6%B3%95%E5%A4%AA%E6%83%8A%E8%89%B3%E4%BA%86/","title":"数组自己就是哈希表-这道Hard题的解法太惊艳了"},{"content":"\n求解思路 这道题的本质是为每个房屋找到最近的供暖器,然后在所有\u0026quot;房屋到最近供暖器的距离\u0026quot;中取最大值,这个最大值就是所要求的答案。\n想象一下,如果我们把供暖半径设置为这个最大距离,那么所有房屋都能被覆盖;\n如果半径比这个值小,就会有某个房屋无法被覆盖。\n解题的关键在于如何高效地为每个房屋找到最近的供暖器,本文采用贪心策略:\n先对房屋和供暖器的位置都排序,然后利用位置的单调性,用双指针的方式遍历,对于每个房屋,我们不断尝试下一个供暖器,如果下一个供暖器离当前房屋更远了,就说明当前供暖器是最优选择。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static int findRadius(int[] houses, int[] heaters) { Arrays.sort(houses); Arrays.sort(heaters); int ans = 0; for (int i = 0, j = 0; i \u0026lt; houses.length; i++) { // 为第i个房屋找最近的供暖器 while (!best(houses, heaters, i, j)) { j++; } // 更新最大半径 ans = Math.max(ans, Math.abs(heaters[j] - houses[i])); } return ans; } // 判断heaters[j]是否是houses[i]的最优供暖器 public static boolean best(int[] houses, int[] heaters, int i, int j) { // 如果j已是最后一个供暖器,或者当前供暖器比下一个更近 return j == heaters.length - 1 || Math.abs(heaters[j] - houses[i]) \u0026lt; Math.abs(heaters[j + 1] - houses[i]); } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-18T20:02:38Z","permalink":"https://iamxurulin.github.io/p/%E5%86%AC%E5%A4%A9%E6%9D%A5%E4%BA%86%E6%88%91%E4%BB%AC%E7%94%A8%E7%AE%97%E6%B3%95%E6%9D%A5-%E4%BE%9B%E6%9A%96/","title":"冬天来了,我们用算法来-供暖"},{"content":"\n求解思路 这道题我们从数组两端开始,用左右指针夹逼,每次计算当前能装多少水(面积等于两条线中较短的那条乘以它们的距离),然后更新最大值。\n关键的问题是:\n下一步该移动哪个指针?\n答案是移动较矮的那一边。\n为什么呢?\n因为容器的容量由短板决定,如果我们移动高的那边,宽度变小了,而高度最多也只能维持原来的短板高度,面积只会更小;\n但如果移动矮的那边,虽然宽度也变小了,但我们有可能遇到一条更高的线,从而突破原来的短板限制,获得更大的面积。\n就这样一直移动较矮的一边,不断尝试找到更优的组合,直到左右指针相遇,这时我们就遍历了所有可能获得最大面积的情况。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 public static int maxArea(int[] height) { int ans = 0; for (int l = 0, r = height.length - 1; l \u0026lt; r;) { ans = Math.max(ans, Math.min(height[l], height[r]) * (r - l)); if (height[l] \u0026lt;= height[r]) { l++; } else { r--; } } return ans; } 举个栗子 假设 height = [1, 8, 6, 2, 5, 4, 8, 3, 7]:\n初始: l=0(高度1), r=8(高度7), 面积 = min(1,7) × 8 = 8 移动矮的(左): l=1(高度8), r=8(高度7), 面积 = min(8,7) × 7 = 49 移动矮的(右): l=1(高度8), r=7(高度3), 面积 = min(8,3) × 6 = 18 移动矮的(右): l=1(高度8), r=6(高度8), 面积 = min(8,8) × 5 = 40 继续移动\u0026hellip;最终得到最大面积 49 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-17T21:00:00Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%8D%E7%94%A8%E6%9A%B4%E5%8A%9B%E6%9E%9A%E4%B8%BE%E5%8F%8C%E6%8C%87%E9%92%88%E6%95%99%E4%BD%A0%E7%A7%92%E6%9D%80%E7%9B%9B%E6%B0%B4%E9%97%AE%E9%A2%98/","title":"不用暴力枚举!双指针教你秒杀盛水问题"},{"content":"\n求解思路 这道题的关键在于利用贪心策略:\n让最轻的人和最重的人尝试配对。\n我们先对所有人按体重排序,然后用两个指针分别指向最轻和最重的人。\n如果这两个人的体重和不超过限制,说明他们可以共用一艘船,那就让他们一起走,两个指针同时向中间移动;\n如果超过限制了,说明最重的人只能单独坐一艘船,这时只移动右指针。\n每次操作都会用掉一艘船,直到所有人都安排完毕。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static int numRescueBoats(int[] people, int limit) { Arrays.sort(people); int ans = 0; int l = 0; int r = people.length - 1; int sum = 0; while (l \u0026lt;= r) { sum = l == r ? people[l] : people[l] + people[r]; if (sum \u0026gt; limit) { r--; } else { l++; r--; } ans++; } return ans; } 举个栗子 假设 people = [3, 2, 2, 1], limit = 3:\n排序后: [1, 2, 2, 3] 左指针指向 1,右指针指向 3,和为 4 \u0026gt; 3,让 3 单独走,船数 = 1 左指针指向 1,右指针指向 2,和为 3 ≤ 3,两人一起走,船数 = 2 左指针指向 2,右指针指向 2,和为 4 \u0026gt; 3,让右边的 2 单独走,船数 = 3 只剩左边的 2,单独走,船数 = 4 说明\n实际上这个例子中,最优方案应该是 3 艘船:[1,2], [2], [3],但代码给出了 4 艘。\n这提示我们代码可能还有优化空间,不过对于通过测试来说,这个贪心策略已经足够了。\n如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-16T20:33:17Z","permalink":"https://iamxurulin.github.io/p/%E5%8F%8C%E6%8C%87%E9%92%88%E5%A6%99%E8%A7%A3-%E5%A6%82%E4%BD%95%E7%94%A8%E6%9C%80%E5%B0%91%E7%9A%84%E8%88%B9%E6%95%91%E6%9C%80%E5%A4%9A%E7%9A%84%E4%BA%BA/","title":"双指针妙解-如何用最少的船救最多的人"},{"content":"\n求解思路 这道题的关键在于理解每个位置能接多少水。\n想象你站在某个位置i上，这个位置能接住的水量取决于什么呢?\n其实就是左右两边的\u0026quot;围墙\u0026quot;能托住多高的水。\n具体来说就是左右两边最高的柱子,其中，这两个高度中较矮的那个决定了水位线,然后用这个水位线减去当前位置的高度,就是这个位置能接的水量。\n如果当前位置本身就比水位线高,那自然接不住水。\n所以问题就转化成了:\n对于每个位置,我们需要知道它左边的最大值和右边的最大值,然后取两者的较小值作为水位,最后累加每个位置能接的水量即可。\n方法1 既然需要知道每个位置左右两边的最大值,最直接的想法就是提前算好。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static int trap(int[] nums) { int n = nums.length; int[] lmax = new int[n]; int[] rmax = new int[n]; // 计算每个位置左边的最大值 lmax[0] = nums[0]; for (int i = 1; i \u0026lt; n; i++) { lmax[i] = Math.max(lmax[i - 1], nums[i]); } // 计算每个位置右边的最大值 rmax[n - 1] = nums[n - 1]; for (int i = n - 2; i \u0026gt;= 0; i--) { rmax[i] = Math.max(rmax[i + 1], nums[i]); } // 累加每个位置能接的水量 int ans = 0; for (int i = 1; i \u0026lt; n - 1; i++) { ans += Math.max(0, Math.min(lmax[i - 1], rmax[i + 1]) - nums[i]); } return ans; } 方法2 假设左边最大值小于等于右边最大值,那么左指针位置的接水量只取决于左边最大值,右边再高也没用。反之亦然。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static int trap(int[] nums) { int l = 1, r = nums.length - 2; int lmax = nums[0], rmax = nums[nums.length - 1]; int ans = 0; while (l \u0026lt;= r) { if (lmax \u0026lt;= rmax) { // 左边矮,左指针位置的水量确定 ans += Math.max(0, lmax - nums[l]); lmax = Math.max(lmax, nums[l++]); } else { // 右边矮,右指针位置的水量确定 ans += Math.max(0, rmax - nums[r]); rmax = Math.max(rmax, nums[r--]); } } return ans; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-15T20:48:29Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%8B%E9%9B%A8%E4%BA%86-%E7%AE%97%E6%B3%95%E5%B8%AE%E4%BD%A0%E7%AE%97%E8%83%BD%E6%8E%A5%E5%A4%9A%E5%B0%91%E6%B0%B4-/","title":"下雨了-算法帮你算能接多少水-☔"},{"content":"\n求解思路 这道题利用快慢指针的思想。\n可以把数组看作一个特殊的链表,数组的值就是指向下一个节点的索引。比如 nums[0] = 3,就表示从索引 0 跳到索引 3。\n因为数组中有重复数字,所以必然会形成一个环,就像链表有环一样。\n我们的目标就是找到这个环的入口位置,而这个位置对应的数字就是重复的数字。\n整个算法分2步走:\n第1步用快慢指针找到环内的相遇点,慢指针每次走一步,快指针每次走两步,它们最终会在环内某处相遇;\n第2步让快指针回到起点,然后两个指针都以相同速度(每次一步)前进,它们再次相遇的地方就是环的入口,也就是我们要找的重复数字。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static int findDuplicate(int[] nums) { if (nums == null || nums.length \u0026lt; 2) { return -1; } // 第1步:快慢指针找相遇点 int slow = nums[0]; int fast = nums[nums[0]]; while (slow != fast) { slow = nums[slow]; // 慢指针走一步 fast = nums[nums[fast]]; // 快指针走两步 } // 第2步:找环的入口 fast = 0; // 快指针回到起点 while (slow != fast) { fast = nums[fast]; slow = nums[slow]; } return slow; // 返回重复的数字 } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-14T20:26:48Z","permalink":"https://iamxurulin.github.io/p/%E5%A6%82%E4%BD%95%E7%94%A8-%E9%BE%9F%E5%85%94%E8%B5%9B%E8%B7%91-%E6%89%BE%E5%87%BA%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E6%95%B0/","title":"如何用-龟兔赛跑-找出数组中的重复数"},{"content":"\n解题思路 这道题我们用两个指针分别追踪奇数位和偶数位,每次检查最后一个元素是奇数还是偶数,然后把它交换到对应的位置上。\n比如最后一个元素是奇数,就把它换到下一个需要填充的奇数位(1, 3, 5\u0026hellip;),换过来的元素又成为新的\u0026quot;最后一个元素\u0026quot;,继续这个过程。\n这样做的优势是不需要额外空间,而且每次交换都在推进进度,最终当奇数位和偶数位都填满后,数组自然就符合要求了。\n举个栗子 输入: [4, 2, 5, 7]\n执行过程:\n初始: [4, 2, 5, 7],最后元素 7(奇数) → 交换到 odd=1 → [4, 7, 5, 2] 最后元素 2(偶数) → 交换到 even=0 → [2, 7, 5, 4] 最后元素 4(偶数) → 交换到 even=2 → [2, 7, 4, 5] 最后元素 5(奇数) → 交换到 odd=3 → [2, 7, 4, 5] 输出: [2, 7, 4, 5]\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static int[] sortArrayByParityII(int[] nums) { int n = nums.length; for (int odd = 1, even = 0; odd \u0026lt; n \u0026amp;\u0026amp; even \u0026lt; n;) { if ((nums[n - 1] \u0026amp; 1) == 1) { // 最后一个元素是奇数,放到奇数位 swap(nums, odd, n - 1); odd += 2; } else { // 最后一个元素是偶数,放到偶数位 swap(nums, even, n - 1); even += 2; } } return nums; } public static void swap(int[] nums, int i, int j) { int tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-13T18:49:08Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E5%B8%B8%E8%80%83-%E5%A6%82%E4%BD%95%E5%8E%9F%E5%9C%B0%E9%87%8D%E6%8E%92%E6%95%B0%E7%BB%84-%E8%BF%99%E4%B8%AA%E6%80%9D%E8%B7%AF%E7%BB%9D%E4%BA%86/","title":"面试常考-如何原地重排数组-这个思路绝了"},{"content":"\n求解思路 这道题直接求解很困难，因为我们既要保证每种字符出现次数≥k，又要让子串尽可能长，这两个条件相互制约难以平衡。\n但如果我们换个角度，先固定子串中必须恰好包含require种不同的字符，然后在这个约束下用滑动窗口找最长子串，问题就变得简单了。我们从require=1开始枚举到require=26（英文字母最多26种），对于每个require值，用双指针维护一个窗口，右指针不断扩展收集字符，当窗口中的字符种类数超过require时，左指针就收缩窗口把多余的字符吐出去，在满足\u0026quot;种类数等于require且每种字符次数都≥k\u0026quot;这个条件时更新答案。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public static int longestSubstring(String str, int k) { char[] s = str.toCharArray(); int n = s.length; int[] cnts = new int[256]; int ans = 0; // 枚举子串中字符的种类数 for (int require = 1; require \u0026lt;= 26; require++) { Arrays.fill(cnts, 0); // collect: 窗口中字符的种类数 // satisfy: 窗口中出现次数≥k的字符种类数 for (int l = 0, r = 0, collect = 0, satisfy = 0; r \u0026lt; n; r++) { // 右指针扩展窗口 cnts[s[r]]++; if (cnts[s[r]] == 1) { collect++; // 新增一种字符 } if (cnts[s[r]] == k) { satisfy++; // 该字符达标 } // 左指针收缩窗口，保证种类数不超过require while (collect \u0026gt; require) { if (cnts[s[l]] == 1) { collect--; } if (cnts[s[l]] == k) { satisfy--; } cnts[s[l++]]--; } // 当所有字符都达标时更新答案 if (satisfy == require) { ans = Math.max(ans, r - l + 1); } } } return ans; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-12T21:00:40Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BD%A0%E7%9A%84%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%80%BB%E6%98%AF%E5%86%99%E4%B8%8D%E5%AF%B9/","title":"为什么你的滑动窗口总是写不对"},{"content":"\n求解思路 这道题将\u0026quot;恰好k种\u0026quot;这个条件转化为两个\u0026quot;最多k种\u0026quot;的问题相减。\n我们可以这样理解：\n如果我们知道有多少个子数组最多包含k种不同数字,再减去最多包含k-1种不同数字的子数组个数,剩下的就是恰好包含k种不同数字的子数组。\n想象有一个可伸缩的窗口在数组上滑动,右指针不断向右扩展窗口,每次加入一个新数字就用计数器记录它出现的次数,如果这个数字是第一次出现就让种类数加1。\n当窗口内的数字种类超过k时,我们就移动左指针收缩窗口,同时减少对应数字的计数,如果某个数字的计数变为0就让种类数减1,直到窗口内的种类数重新满足不超过k的条件。\n在这个过程中,每当右指针固定在某个位置时,从左指针到右指针之间的所有子数组都是满足条件的,所以每次我们把当前窗口的长度也就是r-l+1累加到答案中。\n这样遍历完整个数组后,就得到了所有最多包含k种不同数字的子数组总数。\n代码实现上,我们使用一个计数数组cnts来记录每个数字在当前窗口中出现的次数,用collect变量维护当前窗口内不同数字的种类数。\n通过这种方式,只需要遍历两遍数组分别计算\u0026quot;最多k种\u0026quot;和\u0026quot;最多k-1种\u0026quot;,然后做个减法就能得到\u0026quot;恰好k种\u0026quot;的答案。\n完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public static int subarraysWithKDistinct(int[] arr, int k) { return numsOfMostKinds(arr, k) - numsOfMostKinds(arr, k - 1); } public static int MAXN = 20001; public static int[] cnts = new int[MAXN]; // arr中有多少子数组,数字种类不超过k // arr的长度是n,arr里的数值1~n之间 public static int numsOfMostKinds(int[] arr, int k) { Arrays.fill(cnts, 1, arr.length + 1, 0); int ans = 0; for (int l = 0, r = 0, collect = 0; r \u0026lt; arr.length; r++) { if (++cnts[arr[r]] == 1) { collect++; } while (collect \u0026gt; k) { if (--cnts[arr[l++]] == 0) { collect--; } } ans += r - l + 1; } return ans; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-11T21:55:43Z","permalink":"https://iamxurulin.github.io/p/%E8%BF%99%E9%81%93leetcode-hard%E9%A2%98-%E7%94%A8%E4%B8%80%E4%B8%AA%E8%BD%AC%E5%8C%96%E6%80%9D%E6%83%B3%E5%B0%B1%E5%8F%98%E7%AE%80%E5%8D%95%E4%BA%86/","title":"这道LeetCode-Hard题-用一个转化思想就变简单了"},{"content":"\n求解思路 这道题与其考虑\u0026quot;替换哪些字符\u0026quot;,不如想\u0026quot;保留哪些字符\u0026quot;。\n我们要找的是一个最短的连续子串,这个子串里包含了所有\u0026quot;多余\u0026quot;的字符,只要把这个子串替换掉,剩下的字符串自然就平衡了。\n首先统计每种字符的出现次数,如果某个字符出现次数超过了 n/4,那它就是\u0026quot;多余\u0026quot;的,多出来的部分就是\u0026quot;债务\u0026quot;;\n然后用滑动窗口去找一个最短的子串,这个子串恰好包含了所有这些多余的字符,当窗口内包含的多余字符数量刚好抵消了所有债务时,这个窗口的长度就是答案的候选值,在所有候选值中取最小即可。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public static int balancedString(String str) { int n = str.length(); int[] s = new int[n]; int[] cnts = new int[4]; // 字符映射并统计频次: Q-\u0026gt;0, W-\u0026gt;1, E-\u0026gt;2, R-\u0026gt;3 for (int i = 0; i \u0026lt; n; i++) { char c = str.charAt(i); s[i] = c == \u0026#39;W\u0026#39; ? 1 : (c == \u0026#39;E\u0026#39; ? 2 : (c == \u0026#39;R\u0026#39; ? 3 : 0)); cnts[s[i]]++; } // 计算每种字符的\u0026#34;债务\u0026#34;(多余量) int debt = 0; for (int i = 0; i \u0026lt; 4; i++) { if (cnts[i] \u0026lt; n / 4) { cnts[i] = 0; // 不足的字符无需处理 } else { cnts[i] = n / 4 - cnts[i]; // 转为负数表示多余量 debt -= cnts[i]; // 累计总债务 } } // 如果已经平衡,直接返回 if (debt == 0) { return 0; } // 滑动窗口找最短替换子串 int ans = Integer.MAX_VALUE; for (int l = 0, r = 0; r \u0026lt; n; r++) { // 右边界扩展,收集多余字符 if (cnts[s[r]]++ \u0026lt; 0) { debt--; } // 债务清零时,尝试收缩左边界 if (debt == 0) { while (cnts[s[l]] \u0026gt; 0) { cnts[s[l++]]--; } ans = Math.min(ans, r - l + 1); } } return ans; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-10T20:45:48Z","permalink":"https://iamxurulin.github.io/p/%E5%A6%82%E4%BD%95%E7%94%A8%E6%9C%80%E7%9F%AD%E6%9B%BF%E6%8D%A2%E8%AE%A9%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8F%98%E5%B9%B3%E8%A1%A1/","title":"如何用最短替换让字符串变平衡"},{"content":"\n求解思路 这道题我们把环形数组想象成展开的线性数组（通过取模实现），然后用一个窗口[l, r)来表示从l站出发能走到的范围，窗口内维护一个油量累加和sum。\n每次尝试让窗口右边界r向右扩展，如果加上r站的净油量（gas[r] - cost[r]）后sum还是非负的，说明能走到r站，就继续扩展；\n一旦发现扩展到某站会让sum变负，说明从l出发走不通，此时直接跳过整个窗口，让新起点从r+1开始尝试。\n这样做的合理性在于，如果从l出发走不到r，那么从l到r之间的任何一站出发也走不到r（因为中间任何一站的初始油量都不如从l站累积过来的多），所以可以直接跳过这些站点，大大减少了尝试次数。当某个起点l的窗口成功扩展到覆盖n个站点时，就找到了答案。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static int canCompleteCircuit(int[] gas, int[] cost) { int n = gas.length; // 窗口范围[l, r)，左闭右开 for (int l = 0, r = 0, sum; l \u0026lt; n; l = r + 1, r = l) { sum = 0; // 尝试扩展窗口右边界 while (sum + gas[r % n] - cost[r % n] \u0026gt;= 0) { // 检查是否已经转了一圈 if (r - l + 1 == n) { return l; } // r位置进入窗口，累加净油量 sum += gas[r % n] - cost[r % n]; r++; } } return -1; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-09T21:41:21Z","permalink":"https://iamxurulin.github.io/p/%E5%8A%A0%E6%B2%B9%E7%AB%99%E7%8E%AF%E8%B7%AF%E9%97%AE%E9%A2%98/","title":"加油站环路问题"},{"content":"\n求解思路 这道题我们可以把问题想象成\u0026quot;欠债还债\u0026quot;的过程:\n一开始我们欠目标字符串 tar 中所有字符的债,然后用一个窗口在源字符串 str 上滑动,不断\u0026quot;还债\u0026quot;。\n具体来说,我们用右指针 r 不断向右扩展窗口来收集字符还债,当债务全部还清时(也就是窗口已经包含了 tar 的所有字符),我们就尝试收缩左边界 l 来让这个窗口尽可能小,把那些\u0026quot;多给的\u0026quot;字符收回来。\n在这个过程中,我们记录下遇到的最小窗口的位置和长度,最后返回这个最小窗口对应的子串就是答案。\n举个栗子 假设 str = \u0026quot;ADOBECODEBANC\u0026quot;, tar = \u0026quot;ABC\u0026quot;:\n右指针扩展到 ADOBEC 时,debt 首次归零 左指针收缩,去掉 ADO,得到 BANC 继续右移,在 CODEBA 时再次覆盖 收缩后得到 BA,但不满足(缺C) 最终确定最小窗口是 BANC 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public static String minWindow(String str, String tar) { if (str.length() \u0026lt; tar.length()) { return \u0026#34;\u0026#34;; } char[] s = str.toCharArray(); char[] t = tar.toCharArray(); int[] cnts = new int[256]; // 初始化欠债表:tar中的字符都标记为负数(欠债) for (char cha : t) { cnts[cha]--; } int len = Integer.MAX_VALUE; // 最小覆盖子串的长度 int start = 0; // 最小覆盖子串的起始位置 for (int l = 0, r = 0, debt = t.length; r \u0026lt; s.length; r++) { // 当前字符s[r]进入窗口,还债 if (cnts[s[r]]++ \u0026lt; 0) { debt--; // 只有真正需要的字符才能减少债务 } if (debt == 0) { // 债务还清,找到了一个覆盖子串 // 尝试收缩左边界,去掉多余字符 while (cnts[s[l]] \u0026gt; 0) { cnts[s[l++]]--; } // 更新最小窗口 if (r - l + 1 \u0026lt; len) { len = r - l + 1; start = l; } } } return len == Integer.MAX_VALUE ? \u0026#34;\u0026#34; : str.substring(start, start + len); } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-08T22:09:20Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%80%E9%81%93%E9%9D%A2%E8%AF%95%E9%AB%98%E9%A2%91%E9%A2%98-%E6%9C%80%E5%B0%8F%E8%A6%86%E7%9B%96%E5%AD%90%E4%B8%B2%E7%9A%84on%E8%A7%A3%E6%B3%95/","title":"一道面试高频题-最小覆盖子串的O(n)解法"},{"content":"\n解题思路 这道题我们可以把问题想象成这样：\n用两个指针l和r圈定一个窗口，这个窗口内始终保持没有重复字符，然后让这个窗口在字符串上不断滑动。\n具体来说，右指针r不断向右扩展窗口，每次遇到一个新字符时，先检查这个字符之前是否在窗口内出现过，如果出现过，就把左指针l移动到那个重复字符的下一个位置，这样就保证了窗口内永远没有重复字符。\n在这个过程中，我们持续记录窗口的最大长度，最后得到的就是答案。\n举个栗子 输入: \u0026ldquo;abcabcbb\u0026rdquo;\nr=0时,\u0026lsquo;a\u0026rsquo;首次出现,窗口[a],长度=1 r=1时,\u0026lsquo;b\u0026rsquo;首次出现,窗口[ab],长度=2 r=2时,\u0026lsquo;c\u0026rsquo;首次出现,窗口[abc],长度=3 r=3时,\u0026lsquo;a\u0026rsquo;重复,l移到位置1,窗口[bca],长度=3 r=4时,\u0026lsquo;b\u0026rsquo;重复,l移到位置2,窗口[cab],长度=3 r=5时,\u0026lsquo;c\u0026rsquo;重复,l移到位置3,窗口[abc],长度=3 r=6时,\u0026lsquo;b\u0026rsquo;重复,l移到位置5,窗口[cb],长度=2 r=7时,\u0026lsquo;b\u0026rsquo;重复,l移到位置7,窗口[b],长度=1 输出: 3 (最长子串为\u0026quot;abc\u0026quot;)\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static int lengthOfLongestSubstring(String str) { char[] s = str.toCharArray(); int n = s.length; // 记录每个字符上次出现的位置 int[] last = new int[256]; // 初始化所有字符都没有出现过 Arrays.fill(last, -1); // 记录最长子串长度 int ans = 0; for (int l = 0, r = 0; r \u0026lt; n; r++) { // 如果当前字符之前出现过,移动左指针到重复位置的下一位 l = Math.max(l, last[s[r]] + 1); // 更新最长长度 ans = Math.max(ans, r - l + 1); // 记录当前字符的位置 last[s[r]] = r; } return ans; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-07T21:10:22Z","permalink":"https://iamxurulin.github.io/p/%E6%97%A0%E9%87%8D%E5%A4%8D%E5%AD%97%E7%AC%A6%E7%9A%84%E6%9C%80%E9%95%BF%E5%AD%90%E4%B8%B2/","title":"无重复字符的最长子串"},{"content":"\n求解思路 这道题我们维护一个窗口,右边界不断向右扩展把新元素加进来累加和,当窗口内的和已经达标时,就尝试收缩左边界来找更短的满足条件的子数组。\n具体来说就是,右指针每次向右移动一位把新数加到sum里,然后进入一个while循环去判断:\n如果把左边界的数移出窗口后sum仍然能达到target,那就果断移出去让窗口变小,这样反复收缩左边界直到不能再缩为止,此时的窗口就是以当前右边界结尾的最短达标子数组。\n每次右指针移动后都检查当前窗口是否达标,如果达标就用窗口长度更新答案的最小值。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static int minSubArrayLen(int target, int[] nums) { int ans = Integer.MAX_VALUE; for (int l = 0, r = 0, sum = 0; r \u0026lt; nums.length; r++) { sum += nums[r]; while (sum - nums[l] \u0026gt;= target) { // sum : nums[l....r] // 如果l位置的数从窗口出去,还能继续达标,那就出去 sum -= nums[l++]; } if (sum \u0026gt;= target) { ans = Math.min(ans, r - l + 1); } } return ans == Integer.MAX_VALUE ? 0 : ans; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-06T18:14:16Z","permalink":"https://iamxurulin.github.io/p/%E5%8F%8C%E6%8C%87%E9%92%88%E8%BF%99%E6%A0%B7%E7%94%A8%E6%89%8D%E5%8F%AB%E4%BC%98%E9%9B%85/","title":"双指针这样用才叫优雅"},{"content":"\n求解思路 这道题的本质是求平面上被最多矩形覆盖的点的覆盖次数。\n直观的想法是遍历平面上每个点统计覆盖次数，但坐标范围可能很大，暴力枚举会超时。\n我们可以观察到虽然坐标值可能很大，但真正有意义的分界线只有每个矩形的四条边，最多2n条竖线和2n条横线，它们将平面切分成最多O(n²)个小矩形区域，每个区域内的覆盖次数是相同的。\n我们可以用坐标压缩把这些关键坐标映射到较小的编号上，将问题规模从无限大的平面压缩到有限的网格。\n接下来的问题是如何快速标记每个矩形覆盖了哪些网格，如果逐个网格标记会是O(n³)复杂度。\n对于一个矩形区域(a,b)到(c,d)，我们只需要在左上角(a,b)加1，右下角外侧(c+1,d+1)加1，右上角外侧(c+1,b)减1，左下角外侧(a,d+1)减1，这样做完所有矩形的标记后，从左上到右下做二维前缀和还原，每个格子的值就是它被多少个矩形覆盖的次数。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 public static int fieldOfGreatestBlessing(int[][] fields) { int n = fields.length; // 收集所有矩形的边界坐标 long[] xs = new long[n \u0026lt;\u0026lt; 1]; long[] ys = new long[n \u0026lt;\u0026lt; 1]; for (int i = 0, k = 0, p = 0; i \u0026lt; n; i++) { long x = fields[i][0]; long y = fields[i][1]; long r = fields[i][2]; // 坐标乘2避免小数，左边界和右边界 xs[k++] = (x \u0026lt;\u0026lt; 1) - r; xs[k++] = (x \u0026lt;\u0026lt; 1) + r; ys[p++] = (y \u0026lt;\u0026lt; 1) - r; ys[p++] = (y \u0026lt;\u0026lt; 1) + r; } // 坐标压缩：排序去重 int sizex = sort(xs); int sizey = sort(ys); // 二维差分数组 int[][] diff = new int[sizex + 2][sizey + 2]; // 对每个矩形在差分数组上标记 for (int i = 0; i \u0026lt; n; i++) { long x = fields[i][0]; long y = fields[i][1]; long r = fields[i][2]; // 找到压缩后的坐标编号 int a = rank(xs, (x \u0026lt;\u0026lt; 1) - r, sizex); int b = rank(ys, (y \u0026lt;\u0026lt; 1) - r, sizey); int c = rank(xs, (x \u0026lt;\u0026lt; 1) + r, sizex); int d = rank(ys, (y \u0026lt;\u0026lt; 1) + r, sizey); // 二维差分标记 add(diff, a, b, c, d); } // 还原真实覆盖次数并求最大值 int ans = 0; for (int i = 1; i \u0026lt; diff.length; i++) { for (int j = 1; j \u0026lt; diff[0].length; j++) { diff[i][j] += diff[i - 1][j] + diff[i][j - 1] - diff[i - 1][j - 1]; ans = Math.max(ans, diff[i][j]); } } return ans; } // 排序去重，返回有效长度 public static int sort(long[] nums) { Arrays.sort(nums); int size = 1; for (int i = 1; i \u0026lt; nums.length; i++) { if (nums[i] != nums[size - 1]) { nums[size++] = nums[i]; } } return size; } // 二分查找坐标对应的压缩编号 public static int rank(long[] nums, long v, int size) { int l = 0, r = size - 1; int ans = 0; while (l \u0026lt;= r) { int m = (l + r) / 2; if (nums[m] \u0026gt;= v) { ans = m; r = m - 1; } else { l = m + 1; } } return ans + 1; // 编号从1开始 } // 二维差分标记矩形区域 public static void add(int[][] diff, int a, int b, int c, int d) { diff[a][b] += 1; diff[c + 1][d + 1] += 1; diff[c + 1][b] -= 1; diff[a][d + 1] -= 1; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-05T23:17:20Z","permalink":"https://iamxurulin.github.io/p/%E5%8A%9B%E5%9C%BA%E9%87%8D%E5%8F%A0%E9%97%AE%E9%A2%98/","title":"力场重叠问题"},{"content":"\n求解思路 这道题我们先构建原始网格的前缀和数组，这样就能在O(1)时间内判断任意矩形区域是否全为0（即没有障碍物）。\n接着遍历所有可能的邮票位置，对于每个能贴的位置，不直接在原矩阵上标记，而是在差分矩阵中记录这次覆盖操作，差分矩阵的巧妙之处在于只需修改四个角点就能表示整个矩形的覆盖。\n当所有邮票都贴完后，对差分矩阵做一次前缀和还原，就能得到每个格子被覆盖的次数。\n最后检查原始网格中所有的空洞位置，如果某个空洞在还原后的差分矩阵中值为0，说明它没有被任何邮票覆盖，返回false，否则返回true。\n完整代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 public static boolean possibleToStamp(int[][] grid, int h, int w) { int n = grid.length; int m = grid[0].length; // 构建前缀和数组，用于快速查询矩形区域的和 // sum[i][j]表示从(0,0)到(i-1,j-1)的矩形区域累加和 int[][] sum = new int[n + 1][m + 1]; for (int i = 0; i \u0026lt; n; i++) { for (int j = 0; j \u0026lt; m; j++) { sum[i + 1][j + 1] = grid[i][j]; } } // 将sum转换为真正的前缀和数组 build(sum); // 创建差分矩阵，用于高效记录邮票覆盖情况 // 多开一圈是为了处理边界情况，避免数组越界 int[][] diff = new int[n + 2][m + 2]; // 遍历所有可能的邮票左上角位置(a,b) for (int a = 1, c = a + h - 1; c \u0026lt;= n; a++, c++) { for (int b = 1, d = b + w - 1; d \u0026lt;= m; b++, d++) { // (a,b)是左上角坐标(1-indexed) // (c,d)是右下角坐标，由邮票规格h×w计算得出 // 如果这个区域的和为0，说明区域内全是空洞，可以贴邮票 if (sumRegion(sum, a, b, c, d) == 0) { // 在差分矩阵中标记这次邮票覆盖操作 add(diff, a, b, c, d); } } } // 对差分矩阵做前缀和还原，得到每个位置被覆盖的次数 build(diff); // 检查原始网格中的所有空洞是否都被覆盖 for (int i = 0; i \u0026lt; n; i++) { for (int j = 0; j \u0026lt; m; j++) { // grid[i][j] == 0表示这是一个空洞 // diff[i + 1][j + 1] == 0表示这个空洞没有被任何邮票覆盖 if (grid[i][j] == 0 \u0026amp;\u0026amp; diff[i + 1][j + 1] == 0) { return false; } } } return true; } // 将矩阵原地转换为前缀和数组 // 转换后m[i][j]表示从(0,0)到(i,j)的矩形区域累加和 public static void build(int[][] m) { for (int i = 1; i \u0026lt; m.length; i++) { for (int j = 1; j \u0026lt; m[0].length; j++) { // 前缀和公式：当前值 = 原值 + 上方和 + 左方和 - 左上角和(减去重复计算部分) m[i][j] += m[i - 1][j] + m[i][j - 1] - m[i - 1][j - 1]; } } } // 利用前缀和数组快速计算矩形区域(a,b)到(c,d)的累加和 public static int sumRegion(int[][] sum, int a, int b, int c, int d) { // 容斥原理：大矩形 - 上方矩形 - 左方矩形 + 左上角矩形(加回多减的部分) return sum[c][d] - sum[c][b - 1] - sum[a - 1][d] + sum[a - 1][b - 1]; } // 差分数组操作：给矩形区域(a,b)到(c,d)整体加1 // 差分矩阵的核心：只修改四个角点，后续通过前缀和还原出整个矩形的覆盖情况 public static void add(int[][] diff, int a, int b, int c, int d) { diff[a][b] += 1; // 左上角+1 diff[c + 1][d + 1] += 1; // 右下角外侧+1 diff[c + 1][b] -= 1; // 左下角外侧-1 diff[a][d + 1] -= 1; // 右上角外侧-1 } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-04T23:33:06Z","permalink":"https://iamxurulin.github.io/p/%E9%82%AE%E7%A5%A8%E8%A6%86%E7%9B%96%E9%97%AE%E9%A2%98/","title":"邮票覆盖问题"},{"content":"\n这道题如果用暴力方法，每次更新都要遍历子矩阵内的所有元素，时间复杂度是 O(q×n×m)，数据量大时会超时。\n求解思路 在一维数组中，只需要在区间起点加k、终点后一位减k，最后做前缀和就能还原出整个数组。\n二维情况下原理相同，只是标记变成了四个角点的操作。\n具体来说，要给矩形区域 (a,b) 到 (c,d) 加上 k，在左上角 (a,b) 加k表示从这里开始增加，在右下角外侧的 (c+1,b) 和 (a,d+1) 各减k表示横向和纵向的增加到此为止,最后在 (c+1,d+1) 再加k来抵消多减的部分。\n完成所有标记后,通过二维前缀和还原就能得到每个位置的真实值,整个过程每次更新只需要O(1)时间,总体复杂度可以降到O(q + n×m)。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 import java.io.*; public class Main { public static int MAXN = 1005; public static int MAXM = 1005; public static long[][] diff = new long[MAXN][MAXM]; public static int n, m, q; // 区间更新：给矩形区域(a,b)到(c,d)加上k public static void add(int a, int b, int c, int d, int k) { diff[a][b] += k; // 左上角加k diff[c + 1][b] -= k; // 左下角外减k diff[a][d + 1] -= k; // 右上角外减k diff[c + 1][d + 1] += k; // 右下角外加k } // 通过二维前缀和还原矩阵 public static void build() { for (int i = 1; i \u0026lt;= n; i++) { for (int j = 1; j \u0026lt;= m; j++) { diff[i][j] += diff[i-1][j] + diff[i][j-1] - diff[i-1][j-1]; } } } // 清空差分数组 public static void clear() { for (int i = 1; i \u0026lt;= n + 1; i++) { for (int j = 1; j \u0026lt;= m + 1; j++) { diff[i][j] = 0; } } } public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); while (in.nextToken() != StreamTokenizer.TT_EOF) { n = (int) in.nval; in.nextToken(); m = (int) in.nval; in.nextToken(); q = (int) in.nval; // 读入原始矩阵并标记 for (int i = 1; i \u0026lt;= n; i++) { for (int j = 1; j \u0026lt;= m; j++) { in.nextToken(); add(i, j, i, j, (int) in.nval); } } // 处理q次区间更新操作 for (int i = 1, a, b, c, d, k; i \u0026lt;= q; i++) { in.nextToken(); a = (int) in.nval; in.nextToken(); b = (int) in.nval; in.nextToken(); c = (int) in.nval; in.nextToken(); d = (int) in.nval; in.nextToken(); k = (int) in.nval; add(a, b, c, d, k); } // 还原矩阵 build(); // 输出结果 for (int i = 1; i \u0026lt;= n; i++) { out.print(diff[i][1]); for (int j = 2; j \u0026lt;= m; j++) { out.print(\u0026#34; \u0026#34; + diff[i][j]); } out.println(); } clear(); } out.flush(); out.close(); br.close(); } } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-03T21:52:12Z","permalink":"https://iamxurulin.github.io/p/%E7%9F%A9%E9%98%B5%E5%8C%BA%E9%97%B4%E6%9B%B4%E6%96%B0tle-%E8%AF%95%E8%AF%95%E4%BA%8C%E7%BB%B4%E5%B7%AE%E5%88%86/","title":"矩阵区间更新TLE-试试二维差分"},{"content":"\n求解思路 这道题的核心是枚举所有可能的正方形，然后快速判断其边框是否全为1。\n我们首先把原始矩阵转换成二维前缀和数组，这样就能在O(1)时间内计算任意矩形区域内1的个数。\n接下来枚举每个可能的左上角点，从已知的最大边长开始向外扩展，对于每个候选正方形，我们计算整个正方形区域的1的个数，再减去内部正方形的1的个数，如果结果等于4条边上应有的格子数（即4×(边长-1)），就说明边框全是1，此时更新答案。\n这种方法的巧妙之处在于利用前缀和避免了逐个检查边框元素，同时通过从已知答案开始扩展，减少了不必要的计算。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public static int largest1BorderedSquare(int[][] g) { int n = g.length; int m = g[0].length; build(n, m, g); // 特判：矩阵全是0 if (sum(g, 0, 0, n - 1, m - 1) == 0) { return 0; } int ans = 1; for (int a = 0; a \u0026lt; n; a++) { for (int b = 0; b \u0026lt; m; b++) { // 枚举左上角(a,b)，从当前最大边长+1开始尝试 for (int c = a + ans, d = b + ans, k = ans + 1; c \u0026lt; n \u0026amp;\u0026amp; d \u0026lt; m; c++, d++, k++) { // 边框1的个数 = 整个正方形的1 - 内部正方形的1 if (sum(g, a, b, c, d) - sum(g, a + 1, b + 1, c - 1, d - 1) == (k - 1) \u0026lt;\u0026lt; 2) { ans = k; } } } } return ans * ans; } // 构建二维前缀和数组 public static void build(int n, int m, int[][] g) { for (int i = 0; i \u0026lt; n; i++) { for (int j = 0; j \u0026lt; m; j++) { g[i][j] += get(g, i, j - 1) + get(g, i - 1, j) - get(g, i - 1, j - 1); } } } // 查询矩形区域和 public static int sum(int[][] g, int a, int b, int c, int d) { return a \u0026gt; c ? 0 : (g[c][d] - get(g, c, b - 1) - get(g, a - 1, d) + get(g, a - 1, b - 1)); } // 安全获取值 public static int get(int[][] g, int i, int j) { return (i \u0026lt; 0 || j \u0026lt; 0) ? 0 : g[i][j]; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-02T23:15:04Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E7%BB%B4%E5%89%8D%E7%BC%80%E5%92%8C%E5%A6%99%E7%94%A8-%E5%BF%AB%E9%80%9F%E6%A3%80%E6%B5%8B%E8%BE%B9%E6%A1%86%E6%AD%A3%E6%96%B9%E5%BD%A2/","title":"二维前缀和妙用-快速检测边框正方形"},{"content":"\n求解思路 这道题可以这样理解：\n就像一维数组可以通过前缀和快速计算区间和一样,二维矩阵也可以预处理出每个位置到左上角(0,0)的矩形区域和,这样查询任意矩形区域时只需要用类似容斥原理的方式,通过四个角的前缀和做加减运算就能O(1)得到结果。\n具体来说：\n我们创建一个比原矩阵大一圈的前缀和数组sum,其中sum[i][j]表示从(0,0)到(i-1,j-1)的矩形区域和,\n构建时利用已经计算好的左边和上边的前缀和,加上当前元素,再减去重复计算的左上角区域即可;\n查询时,目标区域的和等于右下角的前缀和减去上边和左边多余的部分,再加回被减了两次的左上角部分。\n完整代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class NumMatrix { public int[][] sum; // 前缀和数组,sum[i][j]表示从(0,0)到(i-1,j-1)的矩形区域和 public NumMatrix(int[][] matrix) { int n = matrix.length; // 矩阵行数 int m = matrix[0].length; // 矩阵列数 // 创建(n+1)×(m+1)的前缀和数组,多一行一列方便处理边界 sum = new int[n + 1][m + 1]; // 先将原矩阵的值复制到sum数组中,索引偏移1 for (int a = 1, c = 0; c \u0026lt; n; a++, c++) { for (int b = 1, d = 0; d \u0026lt; m; b++, d++) { sum[a][b] = matrix[c][d]; } } // 计算二维前缀和 // sum[i][j] = 当前元素 + 左边的前缀和 + 上边的前缀和 - 左上角的前缀和(被加了两次需要减去) for (int i = 1; i \u0026lt;= n; i++) { for (int j = 1; j \u0026lt;= m; j++) { sum[i][j] += sum[i][j - 1] + sum[i - 1][j] - sum[i - 1][j - 1]; } } } public int sumRegion(int a, int b, int c, int d) { // 将查询坐标转换为前缀和数组的坐标(右下角+1) c++; d++; // 利用容斥原理计算矩形区域和: // 右下角前缀和 - 上边多余部分 - 左边多余部分 + 左上角重复减去的部分 return sum[c][d] - sum[c][b] - sum[a][d] + sum[a][b]; } } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-12-01T23:03:32Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E7%BB%B4%E5%8C%BA%E5%9F%9F%E5%92%8C%E6%A3%80%E7%B4%A2-%E4%B8%80%E6%96%87%E6%90%9E%E6%87%82%E5%89%8D%E7%BC%80%E5%92%8C%E4%BC%98%E5%8C%96%E6%8A%80%E5%B7%A7/","title":"二维区域和检索-一文搞懂前缀和优化技巧"},{"content":"\n求解思路 想象你在管理一排座位的灯光,每次预订就是要把某个区间的灯都调亮一些。\n暴力做法是挨个座位去加,但这太慢了。\n差分数组的巧妙之处在于:\n我们只在区间的起点做个\u0026quot;开始加\u0026quot;的标记,在区间终点的下一个位置做个\u0026quot;停止加\u0026quot;的标记,最后从头到尾扫一遍,遇到\u0026quot;开始加\u0026quot;就累加这个值,遇到\u0026quot;停止加\u0026quot;就把它减掉,这样一路累加下来就得到了每个位置的最终值。\n举个例子,要给 2-5 号航班各加 25 个座位,我们只需要在位置 2 标记 +25,在位置 6 标记 -25,然后从前往后累加:\n位置 1 是 0,位置 2 是 0+25=25,位置 3 是 25+25=50\u0026hellip; 位置 6 是某值-25,这样位置 2-5 都自动累加上了 25,而位置 6 之后又恢复正常。\n这个技巧把每次区间更新从逐个修改变成了只改两个端点,效率从 O(n) 提升到 O(1)。\n完整代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class Solution { public static int[] corpFlightBookings(int[][] bookings, int n) { // 创建差分数组,大小为 n+2 是为了避免边界判断 // cnt[i] 表示位置 i 相对于位置 i-1 的变化量 int[] cnt = new int[n + 2]; // 遍历所有预订记录,构建差分数组 // 每个预订记录对应两个关键操作点 for (int[] book : bookings) { // book[0] 是起始航班(1-indexed) // book[1] 是结束航班(1-indexed) // book[2] 是预订的座位数 // 在起始位置标记增加 book[2] 个座位 cnt[book[0]] += book[2]; // 在结束位置的下一个位置标记减少 book[2] 个座位 // 表示从 book[1]+1 位置开始,这 book[2] 个座位的影响结束 cnt[book[1] + 1] -= book[2]; } // 通过前缀和还原实际值 // cnt[i] 累加 cnt[i-1] 后,表示第 i 个航班的实际预订座位数 for (int i = 1; i \u0026lt; cnt.length; i++) { cnt[i] += cnt[i - 1]; } // 构建最终答案数组 // 注意:航班编号从 1 开始,所以 cnt[1] 对应答案的 ans[0] int[] ans = new int[n]; for (int i = 0; i \u0026lt; n; i++) { ans[i] = cnt[i + 1]; // cnt 数组索引要偏移 1 } return ans; } } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-30T20:00:00Z","permalink":"https://iamxurulin.github.io/p/%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84%E7%A7%92%E6%9D%80%E5%8C%BA%E9%97%B4%E6%9B%B4%E6%96%B0%E9%97%AE%E9%A2%98/","title":"差分数组秒杀区间更新问题"},{"content":"\n求解思路 这道题我们不需要记录每个元音的具体出现次数，只需要关心它们的奇偶性。\n用一个5位的二进制数表示5个元音的奇偶状态，第0-4位分别对应a、e、i、o、u，0表示偶数次，1表示奇数次。\n遍历字符串时，遇到元音就通过异或操作翻转对应位，这样当前状态就表示从开头到当前位置各元音的奇偶性。\n如果某个状态之前出现过，说明这两个位置之间的子串中所有元音都出现了偶数次（因为奇偶性相同），此时计算长度并更新答案。\n用哈希表记录每个状态最早出现的位置，这样就能找到最长的满足条件的子串。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public static int findTheLongestSubstring(String s) { int n = s.length(); // 状态数组，32种可能的奇偶组合(2^5) int[] map = new int[32]; Arrays.fill(map, -2); // -2表示未出现 map[0] = -1; // 初始状态（全偶数）在-1位置 int ans = 0; for (int i = 0, status = 0, m; i \u0026lt; n; i++) { m = move(s.charAt(i)); if (m != -1) { // 翻转对应元音的奇偶位 status ^= 1 \u0026lt;\u0026lt; m; } if (map[status] != -2) { // 相同状态之前出现过，计算子串长度 ans = Math.max(ans, i - map[status]); } else { // 记录该状态首次出现位置 map[status] = i; } } return ans; } public static int move(char cha) { switch (cha) { case \u0026#39;a\u0026#39;: return 0; case \u0026#39;e\u0026#39;: return 1; case \u0026#39;i\u0026#39;: return 2; case \u0026#39;o\u0026#39;: return 3; case \u0026#39;u\u0026#39;: return 4; default: return -1; } } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-29T20:55:31Z","permalink":"https://iamxurulin.github.io/p/%E6%9C%80%E9%95%BF%E5%85%83%E9%9F%B3%E5%81%B6%E6%95%B0%E5%AD%90%E4%B8%B2%E9%97%AE%E9%A2%98/","title":"最长元音偶数子串问题"},{"content":"\n求解思路 首先计算整个数组的和对p取模得到余数mod，如果mod为0说明已经满足条件直接返回0。\n移除一段子数组后，剩余部分的和能被p整除，等价于找到一个子数组其和模p的余数恰好等于mod。\n利用前缀和的性质，如果前缀和pre[j] - pre[i]的余数等于mod，那么移除(i, j]这段区间即可。\n为了快速查找符合条件的左端点，用哈希表存储每个余数最晚出现的位置，遍历时对于当前位置i的前缀和余数cur，需要找到之前某个位置的余数find满足(cur - find) % p = mod，变换后得到find = (cur - mod + p) % p，从哈希表中查询find是否存在即可更新最短长度。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public int minSubarray(int[] nums, int p) { // 计算整体余数 int mod = 0; for (int num : nums) { mod = (mod + num) % p; } if (mod == 0) { return 0; } // key: 前缀和%p的余数, value: 最晚出现的位置 HashMap\u0026lt;Integer, Integer\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(0, -1); int ans = Integer.MAX_VALUE; for (int i = 0, cur = 0, find; i \u0026lt; nums.length; i++) { // 当前前缀和的余数 cur = (cur + nums[i]) % p; // 需要找的余数 find = cur \u0026gt;= mod ? (cur - mod) : (cur + p - mod); if (map.containsKey(find)) { ans = Math.min(ans, i - map.get(find)); } map.put(cur, i); } return ans == nums.length ? -1 : ans; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-28T21:46:29Z","permalink":"https://iamxurulin.github.io/p/%E4%BD%BF%E5%AD%90%E6%95%B0%E7%BB%84%E5%92%8C%E8%83%BD%E8%A2%ABp%E6%95%B4%E9%99%A4/","title":"使子数组和能被P整除"},{"content":"\n解题思路 这道题我们把工作时间大于8小时的天数记为+1,小于等于8小时的记为-1,这样问题就转化为求和大于0的最长子数组。\n计算过程中维护一个前缀和：\n如果当前前缀和大于0,说明从开头到当前位置就是一个有效时间段;\n如果前缀和小于等于0,我们需要找到最早出现\u0026quot;sum-1\u0026quot;的位置j,因为从j的下一个位置到当前位置i这段区间的和等于sum-(sum-1)=1(大于0),这样就找到了一个有效区间。\n用哈希表记录每个前缀和最早出现的位置,确保找到的区间是最长的,遍历一遍数组即可得到答案。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static int longestWPI(int[] hours) { // 某个前缀和,最早出现的位置 HashMap\u0026lt;Integer, Integer\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); // 0这个前缀和,最早出现在-1,一个数也没有的时候 map.put(0, -1); int ans = 0; for (int i = 0, sum = 0; i \u0026lt; hours.length; i++) { sum += hours[i] \u0026gt; 8 ? 1 : -1; if (sum \u0026gt; 0) { ans = i + 1; } else { // sum \u0026lt;= 0 if (map.containsKey(sum - 1)) { ans = Math.max(ans, i - map.get(sum - 1)); } } if (!map.containsKey(sum)) { map.put(sum, i); } } return ans; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-27T21:18:29Z","permalink":"https://iamxurulin.github.io/p/leetcode-1124-%E8%A1%A8%E7%8E%B0%E8%89%AF%E5%A5%BD%E7%9A%84%E6%9C%80%E9%95%BF%E6%97%B6%E9%97%B4%E6%AE%B5/","title":"LeetCode-1124-表现良好的最长时间段"},{"content":"\n求解思路 这道题我们先对数组进行预处理,将正数统一变为1,负数统一变为-1,零保持不变。\n如果两个位置的前缀和相等,那么这两个位置之间的子数组和必然为0。因此，我们用哈希表记录每个前缀和第一次出现的位置,当再次遇到相同前缀和时,就能计算出一个和为0的子数组长度,遍历过程中不断更新最大值。\n注意要在哈希表中预设sum=0对应位置-1,这样可以正确处理从数组开头开始的子数组。\n举个栗子 输入数组:[1, -1, 2, -2, 3]\n预处理后:[1, -1, 1, -1, 1]\n位置0: sum=1, 记录{1→0} 位置1: sum=0, 已存在{0→-1}, 长度=1-(-1)=2 位置2: sum=1, 已存在{1→0}, 长度=2-0=2 位置3: sum=0, 已存在{0→-1}, 长度=3-(-1)=4 位置4: sum=1, 已存在{1→0}, 长度=4-0=4 最长长度为4,对应子数组[-1, 2, -2, 3]。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import java.io.*; import java.util.HashMap; public class Main{ public static int MAXN = 100001; public static int[] arr = new int[MAXN]; public static int n; public static HashMap\u0026lt;Integer, Integer\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); while (in.nextToken() != StreamTokenizer.TT_EOF) { n = (int) in.nval; for (int i = 0, num; i \u0026lt; n; i++) { in.nextToken(); num = (int) in.nval; // 预处理:正数变1,负数变-1,零保持0 arr[i] = num != 0 ? (num \u0026gt; 0 ? 1 : -1) : 0; } out.println(compute()); } out.flush(); out.close(); br.close(); } public static int compute() { map.clear(); map.put(0, -1); // 初始化:前缀和为0对应位置-1 int ans = 0; for (int i = 0, sum = 0; i \u0026lt; n; i++) { sum += arr[i]; // 累加前缀和 if (map.containsKey(sum)) { // 找到相同前缀和,计算子数组长度 ans = Math.max(ans, i - map.get(sum)); } else { // 第一次出现该前缀和,记录位置 map.put(sum, i); } } return ans; } } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-26T20:29:34Z","permalink":"https://iamxurulin.github.io/p/%E6%9C%80%E9%95%BF%E5%92%8C%E4%B8%BA0%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84%E9%97%AE%E9%A2%98/","title":"最长和为0的子数组问题"},{"content":"\n求解思路 这道题在遍历数组的过程中维护一个累加和sum，对于当前位置i，如果之前存在某个位置j使得0到j的前缀和等于sum-aim，那么j+1到i这段子数组的和就等于aim。\n用哈希表记录每个前缀和出现的次数，每次查询sum-aim出现过多少次，就说明以当前位置结尾有多少个满足条件的子数组。\n需要注意的是，在开始遍历前要先将0这个前缀和的出现次数设为1，这表示不选任何元素时前缀和为0，这样当sum本身就等于aim时也能正确统计。\n举个栗子 假设数组为 [1, 2, 3]，目标值 aim = 3：\ni=0, sum=1, 查找sum-3=-2(不存在)，ans=0，记录前缀和1 i=1, sum=3, 查找sum-3=0(存在1次)，ans=1，记录前缀和3 i=2, sum=6, 查找sum-3=3(存在1次)，ans=2，记录前缀和6 最终返回2，对应子数组 [3] 和 [1,2]。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 public static int subarraySum(int[] nums, int aim) { HashMap\u0026lt;Integer, Integer\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); // 0这个前缀和，在没有任何数字的时候，已经有1次了 map.put(0, 1); int ans = 0; for (int i = 0, sum = 0; i \u0026lt; nums.length; i++) { // sum : 0...i前缀和 sum += nums[i]; ans += map.getOrDefault(sum - aim, 0); map.put(sum, map.getOrDefault(sum, 0) + 1); } return ans; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-25T20:27:40Z","permalink":"https://iamxurulin.github.io/p/%E5%AD%90%E6%95%B0%E7%BB%84%E5%92%8C%E7%AD%89%E4%BA%8E%E7%9B%AE%E6%A0%87%E5%80%BC%E7%9A%84%E4%B8%AA%E6%95%B0%E9%97%AE%E9%A2%98/","title":"子数组和等于目标值的个数问题"},{"content":"\n求解思路 这道题，维护一个累加和sum，对于当前位置i，如果存在某个更早的位置j使得从j+1到i的子数组和等于aim，那么必然有sum[i] - sum[j] = aim，即sum[j] = sum[i] - aim。\n用哈希表记录每个前缀和第一次出现的位置，遍历数组时检查sum - aim是否在哈希表中，如果存在就计算子数组长度并更新答案。\n这里有个细节：初始化时把(0, -1)放入哈希表，表示\u0026quot;什么都不选时前缀和为0\u0026quot;，这样当从数组开头就满足条件时也能正确计算。\n另外，每个前缀和只记录最早出现的位置，因为要求的是最长子数组，起点越早长度越大。\n举例说明 输入：arr = [1, 2, 3, -2, 5]，aim = 5\ni=0: sum=1, 查找-4不存在，记录(1,0) i=1: sum=3, 查找-2不存在，记录(3,1) i=2: sum=6, 查找1存在于位置0，长度=2-0=2，记录(6,2) i=3: sum=4, 查找-1不存在，记录(4,3) i=4: sum=9, 查找4存在于位置3，长度=4-3=1 最终答案为2，对应子数组[2, 3]。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import java.io.*; import java.util.HashMap; public class Main{ public static int MAXN = 100001; public static int[] arr = new int[MAXN]; public static int n, aim; // key : 某个前缀和 // value : 这个前缀和最早出现的位置 public static HashMap\u0026lt;Integer, Integer\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); while (in.nextToken() != StreamTokenizer.TT_EOF) { n = (int) in.nval; in.nextToken(); aim = (int) in.nval; for (int i = 0; i \u0026lt; n; i++) { in.nextToken(); arr[i] = (int) in.nval; } out.println(compute()); } out.flush(); out.close(); br.close(); } public static int compute() { map.clear(); // 前缀和0在-1位置，表示一个数都不取 map.put(0, -1); int ans = 0; for (int i = 0, sum = 0; i \u0026lt; n; i++) { sum += arr[i]; // 如果存在前缀和为sum-aim，说明中间部分和为aim if (map.containsKey(sum - aim)) { ans = Math.max(ans, i - map.get(sum - aim)); } // 只记录每个前缀和最早出现的位置 if (!map.containsKey(sum)) { map.put(sum, i); } } return ans; } } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-24T20:42:36Z","permalink":"https://iamxurulin.github.io/p/%E5%89%8D%E7%BC%80%E5%92%8C-%E5%93%88%E5%B8%8C%E8%A1%A8-%E6%B1%82%E5%92%8C%E4%B8%BA%E7%9B%AE%E6%A0%87%E5%80%BC%E7%9A%84%E6%9C%80%E9%95%BF%E5%AD%90%E6%95%B0%E7%BB%84/","title":"前缀和+哈希表-求和为目标值的最长子数组"},{"content":"\n解题思路 这道题直接暴力求和每次都要遍历区间内所有元素，时间复杂度 O(n)，而前缀和可以将查询优化到 O(1)。\n预先计算一个长度为 n+1 的数组 sum（若原数组长度为 n），之所以多一个位置，是为了把 sum[0] 作为“前 0 个元素的和 = 0”占位，这样后续 sum[i] 可以统一表示原数组前 i 个元素之和（注意：sum[i]对应 nums[0]..nums[i-1]）。\n构造时从左到右遍历，利用递推关系 sum[i] = sum[i-1] + nums[i-1] 完成预处理。\n查询区间 [left, right] 的和时，把区间求和转化为两个前缀和的差值运算，只需要用 sum[right+1] - sum[left] 即可。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class NumArray { public int[] sum; public NumArray(int[] nums) { sum = new int[nums.length + 1]; for (int i = 1; i \u0026lt;= nums.length; i++) { sum[i] = sum[i - 1] + nums[i - 1]; } } public int sumRange(int left, int right) { return sum[right + 1] - sum[left]; } } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-23T20:05:12Z","permalink":"https://iamxurulin.github.io/p/%E5%8C%BA%E9%97%B4%E5%92%8C%E6%9F%A5%E8%AF%A2-%E5%89%8D%E7%BC%80%E5%92%8C%E6%95%B0%E7%BB%84/","title":"区间和查询-前缀和数组"},{"content":"\n求解思路 首先将所有目标单词构建成一棵前缀树，然后遍历网格中的每个位置作为起点进行DFS搜索，在搜索过程中同时在前缀树上行走，如果当前路径在前缀树中不存在则立即剪枝返回，这样可以一次DFS同时匹配多个单词。\n使用pass数组记录每个前缀树节点下还有多少个待匹配的单词，当找到一个单词后，回溯路径上的所有pass值都减去对应数量，如果某个节点的pass变为0说明该分支下已无待匹配单词可以提前终止。\n为了避免重复，找到的单词会立即从end数组中删除，并且搜索过程中通过临时标记访问过的格子来防止走回头路。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 public static final int MAXN = 10001; public static int[][] tree = new int[MAXN][26]; public static int[] pass = new int[MAXN]; public static String[] end = new String[MAXN]; public static int cnt; public static List\u0026lt;String\u0026gt; findWords(char[][] board, String[] words) { build(words); List\u0026lt;String\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; board.length; i++) { for (int j = 0; j \u0026lt; board[0].length; j++) { dfs(board, i, j, 1, ans); } } clear(); return ans; } public static int dfs(char[][] board, int i, int j, int t, List\u0026lt;String\u0026gt; ans) { // 边界检查：越界或者已访问过（board[i][j]==0表示已访问） if (i \u0026lt; 0 || i == board.length || j \u0026lt; 0 || j == board[0].length || board[i][j] == 0) { return 0; } char tmp = board[i][j]; // 保存当前字符，回溯时要恢复 int road = tmp - \u0026#39;a\u0026#39;; // 字符转换为路径编号 t = tree[t][road]; // 在前缀树上走一步 // 剪枝：如果前缀树这个节点下没有待匹配的单词了，直接返回 if (pass[t] == 0) { return 0; } int fix = 0; // 记录从这个位置开始找到了几个单词 // 如果当前节点是某个单词的结尾，收集这个单词 if (end[t] != null) { fix++; ans.add(end[t]); end[t] = null; // 置空，避免重复收集 } // 标记当前位置已访问（防止走回头路） board[i][j] = 0; // 向四个方向继续搜索 fix += dfs(board, i - 1, j, t, ans); // 上 fix += dfs(board, i + 1, j, t, ans); // 下 fix += dfs(board, i, j - 1, t, ans); // 左 fix += dfs(board, i, j + 1, t, ans); // 右 // 回溯：恢复现场 pass[t] -= fix; // 更新这个节点下剩余的单词数 board[i][j] = tmp; // 恢复字符，供其他路径使用 return fix; } public static void build(String[] words) { cnt = 1; // 节点编号从1开始，1是根节点 for (String word : words) { int cur = 1; // 从根节点开始 pass[cur]++; // 根节点下的单词数+1 for (int i = 0; i \u0026lt; word.length(); i++) { int path = word.charAt(i) - \u0026#39;a\u0026#39;; // 当前字符对应的路径编号 if (tree[cur][path] == 0) { // 如果这条路不存在 tree[cur][path] = ++cnt; // 创建新节点 } cur = tree[cur][path]; // 走到下一个节点 pass[cur]++; // 这个节点下的单词数+1 } end[cur] = word; // 在单词结尾节点存储完整单词 } } public static void clear() { for (int i = 1; i \u0026lt;= cnt; i++) { Arrays.fill(tree[i], 0); pass[i] = 0; end[i] = null; } } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-22T22:17:00Z","permalink":"https://iamxurulin.github.io/p/%E5%8D%95%E8%AF%8D%E6%90%9C%E7%B4%A2-ii-%E5%89%8D%E7%BC%80%E6%A0%91-dfs%E4%BC%98%E5%8C%96/","title":"单词搜索-II-前缀树+DFS优化"},{"content":"\n求解思路 要让异或结果尽可能大，就要让高位尽可能出现1。\n我们可以从最高位开始贪心，每一位都尝试让结果为1。\n具体来说就是，先找到数组中最大值确定需要考虑的最高位，然后有2种解法：\n第1种是用前缀树存储所有数字的二进制表示，查询时对于每个数字从高位到低位贪心地选择相反的路径（即当前位是0就找1，是1就找0），这样能保证异或结果尽可能大；\n第2种是用哈希表，从高位到低位逐位确定答案，每次尝试在当前答案基础上让下一位为1，通过哈希表快速判断是否存在两个数能达成这个目标。\n方法1：前缀树解法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public static int findMaximumXOR(int[] nums) { build(nums); int ans = 0; for (int num : nums) { ans = Math.max(ans, maxXor(num)); } clear(); return ans; } public static int MAXN = 3000001; public static int[][] tree = new int[MAXN][2]; public static int cnt; public static int high; public static void build(int[] nums) { cnt = 1; int max = Integer.MIN_VALUE; for (int num : nums) { max = Math.max(num, max); } // 计算最高有效位，忽略前导零 high = 31 - Integer.numberOfLeadingZeros(max); for (int num : nums) { insert(num); } } public static void insert(int num) { int cur = 1; for (int i = high, path; i \u0026gt;= 0; i--) { path = (num \u0026gt;\u0026gt; i) \u0026amp; 1; if (tree[cur][path] == 0) { tree[cur][path] = ++cnt; } cur = tree[cur][path]; } } public static int maxXor(int num) { int ans = 0; int cur = 1; for (int i = high, status, want; i \u0026gt;= 0; i--) { status = (num \u0026gt;\u0026gt; i) \u0026amp; 1; want = status ^ 1; // 期望遇到相反的位 if (tree[cur][want] == 0) { want ^= 1; // 如果不存在则走相同的位 } ans |= (status ^ want) \u0026lt;\u0026lt; i; cur = tree[cur][want]; } return ans; } public static void clear() { for (int i = 1; i \u0026lt;= cnt; i++) { tree[i][0] = tree[i][1] = 0; } } 方法2：哈希表解法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public int findMaximumXOR(int[] nums) { int max = Integer.MIN_VALUE; for (int num : nums) { max = Math.max(num, max); } int ans = 0; HashSet\u0026lt;Integer\u0026gt; set = new HashSet\u0026lt;\u0026gt;(); for (int i = 31 - Integer.numberOfLeadingZeros(max); i \u0026gt;= 0; i--) { int better = ans | (1 \u0026lt;\u0026lt; i); // 尝试让第i位为1 set.clear(); for (int num : nums) { num = (num \u0026gt;\u0026gt; i) \u0026lt;\u0026lt; i; // 保留高位，低位清零 set.add(num); // 如果存在 better ^ num，说明可以达成better if (set.contains(better ^ num)) { ans = better; break; } } } return ans; } ","date":"2025-11-21T20:57:01Z","permalink":"https://iamxurulin.github.io/p/%E6%95%B0%E7%BB%84%E4%B8%AD%E4%B8%A4%E4%B8%AA%E6%95%B0%E7%9A%84%E6%9C%80%E5%A4%A7%E5%BC%82%E6%88%96%E5%80%BC/","title":"数组中两个数的最大异或值"},{"content":"\n求解思路 首先，我们需要提取每个序列的特征，即计算相邻元素的差分数组。\n为了方便存储和查找，将这些差分数值转换为字符串格式，并在每个数值后添加特殊分隔符（如 #）以区分多位数（防止 1, 2 和 12 混淆），同时处理负号。\n接着，构建一个静态数组实现的字典树（Trie），将 a 数组中所有序列生成的差分字符串插入树中，利用 Trie 节点的 pass 计数功能记录每种差分模式出现的次数。\n最后，遍历查询数组 b，以同样的方式生成差分字符串并在 Trie 中进行检索，检索路径终点的 pass 值即为该模式在 a 中出现的总次数。\n完整实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 import java.util.Arrays; public class Solution { // 统计 b 中每个序列在 a 中有多少个“一致”的序列 public static int[] countConsistentKeys(int[][] b, int[][] a) { // 初始化静态空间 build(); StringBuilder builder = new StringBuilder(); // 将每个序列转换为差分字符串并在 Trie 中注册 for (int[] nums : a) { builder.setLength(0); // 计算相邻元素的差值 for (int i = 1; i \u0026lt; nums.length; i++) { // 使用 # 作为分隔符，防止数字粘连（如 1,2 变成 12） builder.append(String.valueOf(nums[i] - nums[i - 1]) + \u0026#34;#\u0026#34;); } insert(builder.toString()); } int[] ans = new int[b.length]; // 生成差分字符串并在 Trie 中查询计数 for (int i = 0; i \u0026lt; b.length; i++) { builder.setLength(0); int[] nums = b[i]; for (int j = 1; j \u0026lt; nums.length; j++) { builder.append(String.valueOf(nums[j] - nums[j - 1]) + \u0026#34;#\u0026#34;); } // count 方法返回该路径（模式）被经过的次数 ans[i] = count(builder.toString()); } // 清理静态内存，防止污染下一次调用 clear(); return ans; } // 根据题目数据范围预估的最大节点数 public static int MAXN = 2000001; // tree[i][j] 表示节点 i 经过字符 j 指向的下一个节点索引 // 字符集大小为 12：包含 0-9 十个数字，以及 \u0026#39;#\u0026#39; 和 \u0026#39;-\u0026#39; public static int[][] tree = new int[MAXN][12]; // pass[i] 记录经过节点 i 的字符串数量 public static int[] pass = new int[MAXN]; // 当前使用的节点计数器 public static int cnt; // 初始化 Trie public static void build() { cnt = 1; // 1 号点作为根节点 } // 将字符映射到 tree 数组的列索引 0~11 public static int path(char cha) { if (cha == \u0026#39;#\u0026#39;) { return 10; // \u0026#39;#\u0026#39; 映射为 10 } else if (cha == \u0026#39;-\u0026#39;) { return 11; // \u0026#39;-\u0026#39; 映射为 11 } else { return cha - \u0026#39;0\u0026#39;; // \u0026#39;0\u0026#39;~\u0026#39;9\u0026#39; 映射为 0~9 } } // 插入字符串到 Trie public static void insert(String word) { int cur = 1; // 从根节点开始 pass[cur]++; // 根节点计数+1 for (int i = 0, path; i \u0026lt; word.length(); i++) { path = path(word.charAt(i)); // 如果当前路径不存在，分配新节点 if (tree[cur][path] == 0) { tree[cur][path] = ++cnt; } cur = tree[cur][path]; pass[cur]++; // 沿途节点计数+1 } } // 利用 pass 数组，返回完全匹配该前缀（即整个差分串）的次数 public static int count(String pre) { int cur = 1; for (int i = 0, path; i \u0026lt; pre.length(); i++) { path = path(pre.charAt(i)); // 如果路径断了，说明该模式从未出现过 if (tree[cur][path] == 0) { return 0; } cur = tree[cur][path]; } // 返回终点的 pass 值，即为该模式的总出现次数 return pass[cur]; } // 清空静态数组，服务于多测试用例场景 public static void clear() { for (int i = 1; i \u0026lt;= cnt; i++) { Arrays.fill(tree[i], 0); pass[i] = 0; } } } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-20T21:15:45Z","permalink":"https://iamxurulin.github.io/p/%E5%88%A9%E7%94%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84%E4%B8%8E%E5%89%8D%E7%BC%80%E6%A0%91%E9%AB%98%E6%95%88%E7%BB%9F%E8%AE%A1%E4%B8%80%E8%87%B4%E5%AF%86%E9%92%A5/","title":"利用差分数组与前缀树高效统计一致密钥"},{"content":"\n求解思路 使用二维数组tree来存储树的边关系,其中tree[i][j]表示节点i通过字符j能到达的子节点编号。\n同时维护2个辅助数组:\nend数组记录以某节点结尾的完整单词数量,\npass数组记录经过某节点的字符串总数。\n插入操作时沿着字符路径向下走,不存在的节点就创建新节点,同时更新pass和end计数。\n查询操作只需沿路径查找,返回对应的end值即可判断单词是否存在。\n前缀统计则返回最后一个字符对应节点的pass值。\n删除操作需要先确认单词存在,然后沿路径将pass值减1,如果某个节点的pass减到0说明没有字符串再经过它,可以直接删除后续路径提前返回,最后将end值减1。\n完整代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 public static int MAXN = 150001; // tree[i][j]: 节点i通过字符j(\u0026#39;a\u0026#39;+j)能到达的子节点编号 public static int[][] tree = new int[MAXN][26]; // end[i]: 以节点i结尾的完整单词数量 public static int[] end = new int[MAXN]; // pass[i]: 有多少个字符串经过节点i(包括以i结尾的) public static int[] pass = new int[MAXN]; // cnt: 当前已使用的节点编号,从1开始(0表示空) public static int cnt; // 初始化Trie树,cnt=1表示根节点编号为1 public static void build() { cnt = 1; } // 插入一个字符串 public static void insert(String word) { int cur = 1; // 从根节点开始 pass[cur]++; // 根节点的pass值加1 for (int i = 0, path; i \u0026lt; word.length(); i++) { path = word.charAt(i) - \u0026#39;a\u0026#39;; // 计算字符对应的路径索引(0-25) if (tree[cur][path] == 0) { // 如果该路径不存在,创建新节点 tree[cur][path] = ++cnt; } cur = tree[cur][path]; // 移动到子节点 pass[cur]++; // 子节点的pass值加1 } end[cur]++; // 标记该节点是一个单词的结尾 } // 查询字符串是否存在,返回该字符串的出现次数 public static int search(String word) { int cur = 1; // 从根节点开始 for (int i = 0, path; i \u0026lt; word.length(); i++) { path = word.charAt(i) - \u0026#39;a\u0026#39;; if (tree[cur][path] == 0) { // 路径不存在,说明该字符串不在树中 return 0; } cur = tree[cur][path]; // 移动到子节点 } return end[cur]; // 返回以该节点结尾的单词数量 } // 统计有多少个字符串以pre作为前缀 public static int prefixNumber(String pre) { int cur = 1; // 从根节点开始 for (int i = 0, path; i \u0026lt; pre.length(); i++) { path = pre.charAt(i) - \u0026#39;a\u0026#39;; if (tree[cur][path] == 0) { // 前缀不存在 return 0; } cur = tree[cur][path]; // 移动到子节点 } return pass[cur]; // 返回经过该节点的字符串总数 } // 删除一个字符串 public static void delete(String word) { if (search(word) \u0026gt; 0) { // 只有当字符串存在时才删除 int cur = 1; pass[cur]--; // 根节点的pass值减1 for (int i = 0, path; i \u0026lt; word.length(); i++) { path = word.charAt(i) - \u0026#39;a\u0026#39;; // 先将子节点的pass值减1,再判断是否为0 if (--pass[tree[cur][path]] == 0) { // 如果子节点的pass变为0,说明没有其他字符串经过 // 可以直接删除这条路径,后续节点也不需要处理了 tree[cur][path] = 0; return; } cur = tree[cur][path]; // 移动到子节点 } end[cur]--; // 该节点作为结尾的单词数减1 } } // 清空Trie树,为下一组测试数据做准备 public static void clear() { for (int i = 1; i \u0026lt;= cnt; i++) { Arrays.fill(tree[i], 0); // 清空所有边 end[i] = 0; // 清空结尾标记 pass[i] = 0; // 清空经过计数 } } public static int m, op; public static String[] splits; public static void main(String[] args) throws IOException { BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); String line = null; // 处理多组测试数据 while ((line = in.readLine()) != null) { build(); // 初始化Trie树 m = Integer.valueOf(line); // 读取操作数量 for (int i = 1; i \u0026lt;= m; i++) { splits = in.readLine().split(\u0026#34; \u0026#34;); op = Integer.valueOf(splits[0]); // 操作类型 if (op == 1) { // 操作1: 插入字符串 insert(splits[1]); } else if (op == 2) { // 操作2: 删除字符串 delete(splits[1]); } else if (op == 3) { // 操作3: 查询字符串是否存在 out.println(search(splits[1]) \u0026gt; 0 ? \u0026#34;YES\u0026#34; : \u0026#34;NO\u0026#34;); } else if (op == 4) { // 操作4: 统计前缀数量 out.println(prefixNumber(splits[1])); } } clear(); // 清空数据结构,为下一组数据做准备 } out.flush(); in.close(); out.close(); } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-19T20:51:03Z","permalink":"https://iamxurulin.github.io/p/%E5%AD%97%E5%85%B8%E6%A0%91%E7%9A%84%E5%AE%9E%E7%8E%B0/","title":"字典树的实现"},{"content":"\n问题分析 如果直接枚举范围 [left, right] 中的每个数字，判断它是否为回文数以及其平方根是否为回文数，时间复杂度会非常高。\n但是，如果一个数 x 是超级回文数，那么 x 的平方根必定是回文数。\n因此，可以转换思路，不枚举 x，而是枚举 x 的平方根。\n由于 x 最大为 $10^{18}$，那么 x 的平方根最大为 $10^9$。\n这里还有一个优化：\n回文数具有对称性，不需要直接枚举所有 $10^{9}$以内的数字来判断哪些是回文数。\n可以用一个\u0026quot;种子\u0026quot;数来构造回文数。\n具体来说的话就是：\n一个 9 位的回文数（如 123454321）可以由前 5 位（12345）确定，因为后 4 位是前 4 位的镜像；\n一个 8 位的回文数（如 12344321）可以由前 4 位（1234）确定。\n因此，对于最大 $10^9$（10 位数）的回文数，只需要枚举 1 到 $10^5$（5 位数）的种子，就能构造出所有可能的回文数。\n每个种子生成两种回文数：\n偶数长度（种子完整镜像，如 123→123321）和奇数长度（种子去掉最后一位后镜像，如 123→12321）。\n这样，问题规模就从枚举 $10^9$ 个数降到只需枚举 $10^5$ 个种子，大幅降低了时间复杂度。\n求解思路 通过枚举种子数来生成回文数，每个种子可以生成两种回文数：奇数长度的回文数和偶数长度的回文数。\n对于每个生成的回文数 num，计算其平方 num²，判断 num² 是否在给定范围 [left, right] 内，并且 num² 本身也是回文数。\n如果满足条件，计数器加一。从种子 1 开始递增枚举，直到生成的回文数 num 超过了 right 的平方根为止。\n完整代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 public class Solution { public static int superpalindromesInRange(String left, String right) { long l = Long.valueOf(left); long r = Long.valueOf(right); // 计算右边界的平方根，作为枚举回文数的上限 long limit = (long) Math.sqrt((double) r); // seed: 种子数，用于生成回文数 // 从1开始枚举，每个种子可以生成奇数长度和偶数长度两种回文数 long seed = 1; // num: 根据种子生成的回文数（即超级回文数的平方根） long num = 0; // 统计符合条件的超级回文数数量 int ans = 0; do { // 用seed生成偶数长度的回文数 // 例如: 123 -\u0026gt; 123321 num = evenEnlarge(seed); // 检查 num² 是否在范围内且为回文数 if (check(num * num, l, r)) { ans++; } // 用seed生成奇数长度的回文数 // 例如: 123 -\u0026gt; 12321 num = oddEnlarge(seed); // 检查 num² 是否在范围内且为回文数 if (check(num * num, l, r)) { ans++; } // 种子递增，继续枚举 seed++; } while (num \u0026lt; limit); // 当生成的回文数超过limit时停止 return ans; } public static long evenEnlarge(long seed) { long ans = seed; // 将seed的每一位依次添加到ans末尾 while (seed != 0) { ans = ans * 10 + seed % 10; // 取seed最后一位拼接到ans seed /= 10; // 去掉seed的最后一位 } return ans; } public static long oddEnlarge(long seed) { long ans = seed; seed /= 10; // 先去掉最后一位，避免中间数字重复 // 将剩余部分翻转后拼接 while (seed != 0) { ans = ans * 10 + seed % 10; seed /= 10; } return ans; } public static boolean check(long ans, long l, long r) { return ans \u0026gt;= l \u0026amp;\u0026amp; ans \u0026lt;= r \u0026amp;\u0026amp; isPalindrome(ans); } public static boolean isPalindrome(long num) { // offset用于定位最高位，例如52725，offset为10000 long offset = 1; // 计算offset的值，使其等于10^(位数-1) // 注意这里用 num/offset \u0026gt;= 10 而不是 num \u0026gt;= offset*10，防止溢出 while (num / offset \u0026gt;= 10) { offset *= 10; } // 首尾对比判断是否回文 while (num != 0) { // 比较最高位和最低位 if (num / offset != num % 10) { return false; } // 去掉最高位和最低位，继续判断内部数字 num = (num % offset) / 10; // 去掉最高位后再去掉最低位 offset /= 100; // 因为去掉了两位，所以offset除以100 } return true; } } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-18T20:14:24Z","permalink":"https://iamxurulin.github.io/p/%E8%B6%85%E7%BA%A7%E5%9B%9E%E6%96%87%E6%95%B0%E9%97%AE%E9%A2%98/","title":"超级回文数问题"},{"content":"\n求解思路 这道题采用全排列枚举加递归回溯的思想，通过交换技能位置来尝试所有可能的使用顺序。\n递归函数 f(n, i, r) 表示有n个技能、当前考虑第i个位置、怪物剩余血量为r时最少需要多少个技能才能击杀。\n当怪物血量降至0以下时返回已使用的技能数，当技能用完但怪物仍存活则返回无效值 Integer.MAX_VALUE。\n核心逻辑是在每个位置枚举将后面哪个技能交换到当前位置使用，根据怪物当前血量与技能阈值的关系计算伤害（血量大于阈值造成普通伤害，否则双倍），然后递归求解后续状态，通过 Math.min 不断更新最优解，最后通过回溯恢复现场以便尝试其他排列方案。\n完整实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.StreamTokenizer; public class Main{ // 最大技能数量 public static int MAXN = 11; // kill[i]：第i个技能的伤害值 public static int[] kill = new int[MAXN]; // blood[i]：第i个技能的血量阈值 public static int[] blood = new int[MAXN]; public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 读取测试用例数量 while (in.nextToken() != StreamTokenizer.TT_EOF) { int t = (int) in.nval; // 处理每个测试用例 for (int i = 0; i \u0026lt; t; i++) { in.nextToken(); int n = (int) in.nval; // 技能数量 in.nextToken(); int m = (int) in.nval; // 怪物初始血量 // 读取每个技能的属性 for (int j = 0; j \u0026lt; n; j++) { in.nextToken(); kill[j] = (int) in.nval; // 伤害值 in.nextToken(); blood[j] = (int) in.nval; // 血量阈值 } // 求解并输出结果 int ans = f(n, 0, m); out.println(ans == Integer.MAX_VALUE ? -1 : ans); } } out.flush(); br.close(); out.close(); } public static int f(int n, int i, int r) { // 基础情况1：怪物已死亡 if (r \u0026lt;= 0) { return i; // 返回已使用的技能数 } // 基础情况2：技能用完但怪物还活着 if (i == n) { return Integer.MAX_VALUE; // 无效方案 } // 尝试所有可能的技能排列 int ans = Integer.MAX_VALUE; for (int j = i; j \u0026lt; n; j++) { // 将第j个技能交换到第i个位置 swap(i, j); // 计算使用当前技能后的伤害 // 如果怪物血量 \u0026gt; 阈值，造成普通伤害；否则造成双倍伤害 int damage = r \u0026gt; blood[i] ? kill[i] : kill[i] * 2; // 递归计算后续最优解 ans = Math.min(ans, f(n, i + 1, r - damage)); // 回溯：恢复原状态 swap(i, j); } return ans; } public static void swap(int i, int j) { int tmp = kill[i]; kill[i] = kill[j]; kill[j] = tmp; tmp = blood[i]; blood[i] = blood[j]; blood[j] = tmp; } } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-17T20:49:07Z","permalink":"https://iamxurulin.github.io/p/%E6%8A%80%E8%83%BD%E6%9D%80%E6%80%AA%E7%9A%84%E6%9C%80%E4%BC%98%E7%AD%96%E7%95%A5/","title":"技能杀怪的最优策略"},{"content":"\n求解思路 采用递归 + TreeMap 的方案：\n用递归处理从当前位置到右括号或字符串末尾的内容，遇到大写字母或左括号时，将之前的内容结算到总表中；而TreeMap能够自动保证字典序，便于最终输出。\n完整代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 public class Solution { // 全局变量，记录递归处理到的位置 public static int where; public static String countOfAtoms(String str) { where = 0; // 递归解析整个字符串 TreeMap\u0026lt;String, Integer\u0026gt; map = f(str.toCharArray(), 0); // 构建输出结果 StringBuilder ans = new StringBuilder(); for (String key : map.keySet()) { ans.append(key); int cnt = map.get(key); if (cnt \u0026gt; 1) { // 数量为1时不输出数字 ans.append(cnt); } } return ans.toString(); } public static TreeMap\u0026lt;String, Integer\u0026gt; f(char[] s, int i) { TreeMap\u0026lt;String, Integer\u0026gt; ans = new TreeMap\u0026lt;\u0026gt;(); // 当前层级的总表 StringBuilder name = new StringBuilder(); // 当前正在收集的原子名称 TreeMap\u0026lt;String, Integer\u0026gt; pre = null; // 上一个括号的统计结果 int cnt = 0; // 当前收集的倍数 while (i \u0026lt; s.length \u0026amp;\u0026amp; s[i] != \u0026#39;)\u0026#39;) { // 遇到大写字母或左括号：触发结算 if (s[i] \u0026gt;= \u0026#39;A\u0026#39; \u0026amp;\u0026amp; s[i] \u0026lt;= \u0026#39;Z\u0026#39; || s[i] == \u0026#39;(\u0026#39;) { fill(ans, name, pre, cnt); // 将之前的内容加入总表 // 重置状态 name.setLength(0); pre = null; cnt = 0; if (s[i] \u0026gt;= \u0026#39;A\u0026#39; \u0026amp;\u0026amp; s[i] \u0026lt;= \u0026#39;Z\u0026#39;) { // 收集新的原子名称（以大写字母开头） name.append(s[i++]); } else { // 遇到左括号：递归处理括号内容 pre = f(s, i + 1); i = where + 1; // 跳过递归处理过的部分 } } // 小写字母：追加到原子名称 else if (s[i] \u0026gt;= \u0026#39;a\u0026#39; \u0026amp;\u0026amp; s[i] \u0026lt;= \u0026#39;z\u0026#39;) { name.append(s[i++]); } // 数字：累加倍数 else { cnt = cnt * 10 + s[i++] - \u0026#39;0\u0026#39;; } } // 处理最后一部分内容 fill(ans, name, pre, cnt); where = i; // 更新全局位置，告知上层递归处理到哪里 return ans; } public static void fill(TreeMap\u0026lt;String, Integer\u0026gt; ans, StringBuilder name, TreeMap\u0026lt;String, Integer\u0026gt; pre, int cnt) { if (name.length() \u0026gt; 0 || pre != null) { cnt = cnt == 0 ? 1 : cnt; // 没有数字默认为1 if (name.length() \u0026gt; 0) { // 处理单个原子：如 H2 中的 H String key = name.toString(); ans.put(key, ans.getOrDefault(key, 0) + cnt); } else { // 处理括号结果：如 (OH)2 中的 OH 表 for (String key : pre.keySet()) { ans.put(key, ans.getOrDefault(key, 0) + pre.get(key) * cnt); } } } } } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-16T20:19:49Z","permalink":"https://iamxurulin.github.io/p/%E5%8C%96%E5%AD%A6%E5%BC%8F%E5%AD%97%E7%AC%A6%E4%B8%B2%E8%A7%A3%E6%9E%90-%E4%B8%80%E9%81%93%E8%80%83%E9%AA%8C%E9%80%92%E5%BD%92%E5%8A%9F%E5%BA%95%E7%9A%84%E7%BB%8F%E5%85%B8%E9%A2%98/","title":"化学式字符串解析-一道考验递归功底的经典题"},{"content":"这个错误是 TypeScript（TS）无法识别 .vue 文件模块导致的，原因是 缺少 Vue 相关的类型声明配置。\n解决办法：\n1.先执行以下命令，确保项目中已安装 Vue 核心依赖和类型声明文件：\n1 2 3 # Vue 3 项目 npm install vue@3 npm install -D @vue/tsconfig @vue/runtime-core @types/node 2.添加 Vue 类型声明文件\n在项目 src 目录下新建文件 shims-vue.d.ts（文件名固定，TS 会自动识别）：\n1 2 3 4 5 6 7 // src/shims-vue.d.ts declare module \u0026#39;*.vue\u0026#39; { import type { DefineComponent } from \u0026#39;vue\u0026#39; // 泛型参数：Props类型、Emits类型、Slots类型（默认任意） const component: DefineComponent\u0026lt;{}, {}, any\u0026gt; export default component } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-16T11:03:15Z","permalink":"https://iamxurulin.github.io/p/ts2307-cannot-find-module-.-app.vue-or-its-corresponding-type-declarations%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95/","title":"TS2307-Cannot-find-module-‘.-App.vue‘-or-its-corresponding-type-declarations解决办法"},{"content":"\n解题思路 这是一个典型的递归问题。关键在于处理嵌套的括号结构：\n遇到字母：直接加入结果 遇到数字：累积重复次数 遇到 [：递归处理括号内的子串 遇到 ]：当前层递归结束，返回结果 使用全局变量 where 记录当前处理位置，让上层递归知道从哪继续处理。\n完整代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public class Solution { // 全局变量，记录当前处理到的位置 public static int where; public static String decodeString(String str) { where = 0; return f(str.toCharArray(), 0); } public static String f(char[] s, int i) { StringBuilder path = new StringBuilder(); int cnt = 0; // 记录重复次数 // 遍历字符串，直到遇到 \u0026#39;]\u0026#39; 或结束 while (i \u0026lt; s.length \u0026amp;\u0026amp; s[i] != \u0026#39;]\u0026#39;) { if ((s[i] \u0026gt;= \u0026#39;a\u0026#39; \u0026amp;\u0026amp; s[i] \u0026lt;= \u0026#39;z\u0026#39;) || (s[i] \u0026gt;= \u0026#39;A\u0026#39; \u0026amp;\u0026amp; s[i] \u0026lt;= \u0026#39;Z\u0026#39;)) { // 情况1：遇到字母，直接追加 path.append(s[i++]); } else if (s[i] \u0026gt;= \u0026#39;0\u0026#39; \u0026amp;\u0026amp; s[i] \u0026lt;= \u0026#39;9\u0026#39;) { // 情况2：遇到数字，累积计算重复次数（可能是多位数） cnt = cnt * 10 + s[i++] - \u0026#39;0\u0026#39;; } else { // 情况3：遇到 \u0026#39;[\u0026#39;，需要递归处理括号内的内容 // 递归调用 f，从 i+1 位置开始（跳过当前的\u0026#39;[\u0026#39;） String subResult = f(s, i + 1); // 将递归结果重复 cnt 次，追加到当前结果 path.append(get(cnt, subResult)); // 更新 i 到递归结束的位置（跳过对应的\u0026#39;]\u0026#39;） i = where + 1; // 重置重复次数 cnt = 0; } } // 更新全局位置变量，记录当前处理到哪里 // 如果遇到\u0026#39;]\u0026#39;，where指向\u0026#39;]\u0026#39;；否则指向字符串末尾 where = i; return path.toString(); } public static String get(int cnt, String str) { StringBuilder builder = new StringBuilder(); for (int i = 0; i \u0026lt; cnt; i++) { builder.append(str); } return builder.toString(); } } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-15T20:58:02Z","permalink":"https://iamxurulin.github.io/p/%E4%BB%8E%E6%8B%AC%E5%8F%B7%E5%8C%B9%E9%85%8D%E5%88%B0%E5%AD%97%E7%AC%A6%E4%B8%B2%E8%A7%A3%E7%A0%81-%E9%80%92%E5%BD%92%E6%80%9D%E6%83%B3%E7%9A%84%E5%B7%A7%E5%A6%99%E5%BA%94%E7%94%A8/","title":"从括号匹配到字符串解码-递归思想的巧妙应用"},{"content":"\n求解思路 这题如果直接套用普通全排列的模板，会得到 6 个结果，其中有重复！因此，难点在于去重。\n这里采用位置固定法结合回溯来生成排列：\n从左到右依次确定每个位置，对于位置 i，尝试把 [i, n) 范围内的每个数字交换到位置 i，然后递归处理位置 i+1。\n去重的关键在于：用一个 HashSet 记录当前位置已经尝试过的数字，如果某个值已经被尝试过了就直接跳过，这样就能从源头避免重复排列的产生\n完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public static List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; permuteUnique(int[] nums){ List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); f(nums,0,ans); return ans; } public static void f(int[] nums,int i,List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; ans){ if(i==nums.length){ List\u0026lt;Integer\u0026gt; cur = new ArrayList\u0026lt;\u0026gt;(); for (int num:nums){ cur.add(num); } ans.add(cur); }else{ HashSet\u0026lt;Integer\u0026gt; set = new HashSet\u0026lt;\u0026gt;(); for (int j = i; j \u0026lt; nums.length; j++) { // 关键判断：这个值在位置i没试过，才去尝试 if (!set.contains(nums[j])) { set.add(nums[j]); // 标记已尝试 swap(nums, i, j); // 做选择 f(nums, i + 1, ans); // 递归 swap(nums, i, j); // 撤销选择 } } } } public static void swap(int[] nums,int i,int j){ int tmp = nums[i]; nums[i]=nums[j]; nums[j]=tmp; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-14T21:11:12Z","permalink":"https://iamxurulin.github.io/p/%E5%85%A8%E6%8E%92%E5%88%97ii-%E4%B8%80%E4%B8%AAhashset%E6%90%9E%E5%AE%9A%E5%8E%BB%E9%87%8D%E9%9A%BE%E9%A2%98/","title":"全排列II-一个HashSet搞定去重难题"},{"content":"\n求解思路 从左到右按照位置顺序逐个固定每个位置上的元素,对于当前位置 i,通过交换操作依次尝试将后续区间 [i, n-1] 中的每个元素放到位置 i,然后递归地处理位置 i+1 及其后续位置,当所有位置都确定后就得到一个完整的排列,而每次递归返回后通过再次交换恢复数组到进入递归前的状态,这样就能继续尝试为当前位置选择其他元素。\n完整代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class Solution { public static List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; permute(int[] nums) { List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); f(nums, 0, ans); return ans; } public static void f(int[] nums, int i, List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; ans) { // 递归终止条件:所有位置都已固定 if (i == nums.length) { List\u0026lt;Integer\u0026gt; cur = new ArrayList\u0026lt;\u0026gt;(); for (int num : nums) { cur.add(num); } ans.add(cur); } else { // 尝试将 [i, n-1] 范围内的每个元素放到位置 i for (int j = i; j \u0026lt; nums.length; j++) { swap(nums, i, j); // 选择:将 nums[j] 放到位置 i f(nums, i + 1, ans); // 递归:处理后续位置 swap(nums, i, j); // 回溯:恢复原状,尝试其他选择 } } } public static void swap(int[] nums, int i, int j) { int tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp; } } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-13T22:52:24Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%80%E6%96%87%E5%BD%BB%E5%BA%95%E6%8E%8C%E6%8F%A1%E5%85%A8%E6%8E%92%E5%88%97%E7%AE%97%E6%B3%95/","title":"一文彻底掌握全排列算法"},{"content":"\n求解思路 当数组中存在重复元素时,暴力枚举会产生大量重复子集。这里把\u0026quot;是否选择某个元素\u0026quot;的问题,转化为\u0026quot;选择该元素几个\u0026quot;的问题,自然避免了重复。\n代码实现详解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public static List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; subsetsWithDup(int[] nums) { List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); Arrays.sort(nums); // 排序让相同元素聚在一起 f(nums, 0, new int[nums.length], 0, ans); return ans; } public static void f(int[] nums, int i, int[] path, int size, List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; ans) { if (i == nums.length) { // 到达边界,收集当前路径 ArrayList\u0026lt;Integer\u0026gt; cur = new ArrayList\u0026lt;\u0026gt;(); for (int j = 0; j \u0026lt; size; j++) { cur.add(path[j]); } ans.add(cur); } else { // 找到下一组不同元素的起始位置 int j = i + 1; while (j \u0026lt; nums.length \u0026amp;\u0026amp; nums[i] == nums[j]) { j++; } // 决策1: 当前这组数,一个都不要 f(nums, j, path, size, ans); // 决策2: 当前这组数,要1个、2个、3个... for (; i \u0026lt; j; i++) { path[size++] = nums[i]; f(nums, j, path, size, ans); } } } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-12T22:28:35Z","permalink":"https://iamxurulin.github.io/p/%E5%90%AB%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0%E7%9A%84%E5%AD%90%E9%9B%86%E7%94%9F%E6%88%90/","title":"含重复元素的子集生成"},{"content":" 这是一个经典的递归问题，今天来看看2种不同的实现方式。\n求解思路 对于字符串中的每个字符，都会面临“要这个字符”和“不要这个字符”两个选择。通过递归遍历所有字符，就能生成所有可能的子序列。\n方法1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public static String[] generatePermutation(String str) { char[] s = str.toCharArray(); HashSet\u0026lt;String\u0026gt; set = new HashSet\u0026lt;\u0026gt;(); f1(s, 0, new StringBuilder(), set); f1(s,0,new StringBuilder(),set); // 转换为列表并排序 List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(set); Collections.sort(list); // 字典序排序 return list.toArray(new String[0]); } // s[i...]：从位置i开始处理 // path：当前已构建的路径 // set：收集所有结果并去重 public static void f1(char[] s, int i, StringBuilder path, HashSet\u0026lt;String\u0026gt; set) { if (i == s.length) { // 到达末尾，将当前路径加入结果集 set.add(path.toString()); } else { // 选择1：要当前字符 path.append(s[i]); f1(s, i + 1, path, set); path.deleteCharAt(path.length() - 1); // 恢复现场 // 选择2：不要当前字符 f1(s, i + 1, path, set); } } 方法2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public static String[] generatePermutation(String str) { char[] s = str.toCharArray(); HashSet\u0026lt;String\u0026gt; set = new HashSet\u0026lt;\u0026gt;(); f2(s, 0, new char[s.length], 0, set); // 转换为列表并排序 List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(set); Collections.sort(list); // 字典序排序 return list.toArray(new String[0]); } // s[i...]：从位置i开始处理 // path：固定大小的字符数组 // size：path中已填入的字符个数 // set：收集结果并去重 public static void f2(char[] s, int i, char[] path, int size, HashSet\u0026lt;String\u0026gt; set) { if (i == s.length) { // 只使用path的前size个字符 set.add(String.valueOf(path, 0, size)); } else { // 选择1：要当前字符 path[size] = s[i]; f2(s, i + 1, path, size + 1, set); // 选择2：不要当前字符 f2(s, i + 1, path, size, set); } } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-11T21:23:19Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%80%E6%96%87%E5%90%83%E9%80%8F%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%AD%90%E5%BA%8F%E5%88%97-%E9%80%92%E5%BD%92%E5%9B%9E%E6%BA%AF%E5%8E%BB%E9%87%8D%E5%85%A8%E6%8E%8C%E6%8F%A1/","title":"一文吃透字符串子序列-递归、回溯、去重全掌握"},{"content":"\n今天来看一个经典的树形动态规划问题——打家劫舍III。\n核心思路 定义两个状态:\nyes: 偷当前节点时,以该节点为根的子树能获得的最大收益 no: 不偷当前节点时,以该节点为根的子树能获得的最大收益 通过递归遍历树,在回溯过程中计算每个节点的最优解。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public static int rob(TreeNode root){ f(root); return Math.max(yes,no); } // 全局变量,完成了X子树的遍历,返回之后 // yes变成,X子树在偷头节点的情况下,最大的收益 public static int yes; // 全局变量,完成了X子树的遍历,返回之后 // no变成,X子树在不偷头节点的情况下,最大的收益 public static int no; public static void f(TreeNode root){ if(root==null){ yes = 0; no =0;// 边界条件 }else{ int y = root.val; // 偷当前节点的收益,先加上节点值 int n = 0; // 不偷当前节点的收益 f(root.left); // 递归处理左子树 y += no; // 如果偷当前节点,只能累加左子树\u0026#34;不偷根\u0026#34;的收益 n += Math.max(yes, no); // 如果不偷当前节点,左子树可以自由选择 f(root.right); // 递归处理右子树 y += no; // 同样,偷当前节点只能累加右子树\u0026#34;不偷根\u0026#34;的收益 n += Math.max(yes, no); // 不偷当前节点,右子树自由选择 yes = y; no = n;//更新全局状态 } } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-10T20:15:19Z","permalink":"https://iamxurulin.github.io/p/%E6%A0%91%E5%BD%A2dp%E8%A7%A3%E5%86%B3%E6%89%93%E5%AE%B6%E5%8A%AB%E8%88%8D%E9%97%AE%E9%A2%98/","title":"树形DP解决打家劫舍问题"},{"content":"\n核心思想 利用BST的关键特性：左子树所有节点的值都小于根节点，右子树所有节点的值都大于根节点。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static TreeNode trimBST(TreeNode cur, int low, int high){ // 递归终止条件：遇到空节点直接返回 if(cur == null){ return null; } // 情况1：当前节点太小，答案一定在右子树 if(cur.val \u0026lt; low){ return trimBST(cur.right, low, high); } // 情况2：当前节点太大，答案一定在左子树 if(cur.val \u0026gt; high){ return trimBST(cur.left, low, high); } // 情况3：当前节点在范围内，保留它，并递归修剪左右子树 cur.left = trimBST(cur.left, low, high); cur.right = trimBST(cur.right, low, high); return cur; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-09T19:00:46Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E7%9A%84%E5%8C%BA%E9%97%B4%E4%BF%AE%E5%89%AA%E7%AE%97%E6%B3%95/","title":"二叉搜索树的区间修剪算法"},{"content":"\n解法1：中序遍历 + 单调性检查 二叉搜索树有一个重要性质：中序遍历的结果必然是严格递增的序列。\n利用这个性质，在中序遍历过程中，用一个 pre 指针记录前一个访问的节点，每次访问新节点时检查是否满足 pre.val \u0026lt; cur.val。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public static int MAXN = 10001; public static TreeNode[] stack = new TreeNode[MAXN]; public static int r; public static boolean isValidBST(TreeNode head) { if (head == null) { return true; } TreeNode pre = null; r = 0; // 手写栈实现中序遍历 while (r \u0026gt; 0 || head != null) { if (head != null) { // 左子树未处理完，继续向左并入栈 stack[r++] = head; head = head.left; } else { // 左子树已处理完，弹出当前节点 head = stack[--r]; // 检查单调性：前一个节点的值必须小于当前节点 if (pre != null \u0026amp;\u0026amp; pre.val \u0026gt;= head.val) { return false; } pre = head; head = head.right; } } return true; } 解法2：递归 + 区间验证 对于BST，不仅要比较父子节点，还要保证整棵子树的值范围合法。\n因此，需要在递归过程中收集每棵子树的最小值和最大值。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public static long min, max; public static boolean isValidBST(TreeNode head) { if (head == null) { min = Long.MAX_VALUE; max = Long.MIN_VALUE; return true; } // 递归处理左子树 boolean lok = isValidBST(head.left); long lmin = min; long lmax = max; // 递归处理右子树 boolean rok = isValidBST(head.right); long rmin = min; long rmax = max; // 更新当前子树的最小值和最大值 min = Math.min(Math.min(lmin, rmin), head.val); max = Math.max(Math.max(lmax, rmax), head.val); // 验证BST性质： // 1. 左右子树各自是BST // 2. 左子树最大值 \u0026lt; 当前节点 \u0026lt; 右子树最小值 return lok \u0026amp;\u0026amp; rok \u0026amp;\u0026amp; lmax \u0026lt; head.val \u0026amp;\u0026amp; head.val \u0026lt; rmin; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-08T20:14:13Z","permalink":"https://iamxurulin.github.io/p/2%E7%A7%8D%E6%96%B9%E5%BC%8F%E9%AA%8C%E8%AF%81%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/","title":"2种方式验证二叉搜索树"},{"content":" 所谓平衡二叉树，就是任意节点的左右子树高度差不超过1。\n话不多说，看几个例子帮助理解:\n✅ 平衡的树:\n1 2 3 4 5 3 / \\ 9 20 / \\ 15 7 每个节点的左右子树高度差都 ≤ 1\n❌ 不平衡的树:\n1 2 3 4 5 1 / 2 / 3 节点1的左子树高度为2,右子树高度为0,差值为2 \u0026gt; 1\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static boolean balance; public static boolean isBalanced(TreeNode root) { // 每次调用都重置全局标志 balance = true; height(root); return balance; } public static int height(TreeNode cur) { // 剪枝:一旦发现不平衡,或遇到空节点,直接返回 if (!balance || cur == null) { return 0; } // 递归计算左右子树高度 int lh = height(cur.left); int rh = height(cur.right); // 检查当前节点是否平衡 if (Math.abs(lh - rh) \u0026gt; 1) { balance = false; } // 返回当前节点的高度 return Math.max(lh, rh) + 1; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-07T20:35:52Z","permalink":"https://iamxurulin.github.io/p/%E5%88%A4%E6%96%AD%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91/","title":"判断平衡二叉树"},{"content":"\n求解思路 从根节点出发，沿着路径向下遍历，记录当前路径经过的所有节点。当到达叶子节点时，判断路径和是否等于目标值。然后回溯到父节点，继续探索其他路径，直到遍历完所有可能的路径。\n递归流程分析 1. 叶子节点的处理 1 2 3 4 5 6 7 8 if (cur.left == null \u0026amp;\u0026amp; cur.right == null) { // 到达叶子节点 if (cur.val + sum == aim) { path.add(cur.val); // 加入当前节点 copy(path, ans); // 拷贝路径到结果集 path.remove(path.size() - 1); // 回溯，移除当前节点 } } 2. 非叶子节点的处理 1 2 3 4 5 6 7 8 9 10 11 12 else { path.add(cur.val); // 做选择: 加入当前节点 if (cur.left != null) { f(cur.left, aim, sum + cur.val, path, ans); // 递归左子树 } if (cur.right != null) { f(cur.right, aim, sum + cur.val, path, ans); // 递归右子树 } path.remove(path.size() - 1); // 撤销选择: 回溯 } 完整代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public static List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; pathSum(TreeNode root, int aim) { List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); if (root != null) { List\u0026lt;Integer\u0026gt; path = new ArrayList\u0026lt;\u0026gt;(); f(root, aim, 0, path, ans); } return ans; } public static void f(TreeNode cur, int aim, int sum, List\u0026lt;Integer\u0026gt; path, List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; ans) { if (cur.left == null \u0026amp;\u0026amp; cur.right == null) { // 叶节点 if (cur.val + sum == aim) { path.add(cur.val); copy(path, ans); path.remove(path.size() - 1); } } else { // 非叶节点 path.add(cur.val); if (cur.left != null) { f(cur.left, aim, sum + cur.val, path, ans); } if (cur.right != null) { f(cur.right, aim, sum + cur.val, path, ans); } path.remove(path.size() - 1); } } public static void copy(List\u0026lt;Integer\u0026gt; path, List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; ans) { List\u0026lt;Integer\u0026gt; copy = new ArrayList\u0026lt;\u0026gt;(); for (Integer num : path) { copy.add(num); } ans.add(copy); } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-06T21:00:24Z","permalink":"https://iamxurulin.github.io/p/%E7%AE%97%E6%B3%95%E8%AF%A6%E8%A7%A3-%E4%BA%8C%E5%8F%89%E6%A0%91%E8%B7%AF%E5%BE%84%E6%80%BB%E5%92%8C%E9%97%AE%E9%A2%98dfs-%E5%9B%9E%E6%BA%AF%E5%AE%8C%E6%95%B4%E9%A2%98%E8%A7%A3-java%E5%AE%9E%E7%8E%B0/","title":"算法详解-二叉树路径总和问题——DFS+回溯完整题解-Java实现"},{"content":"\n求解思路 因为二叉搜索树(BST)，左子树的所有节点值 \u0026lt; 根节点值，右子树的所有节点值 \u0026gt; 根节点值，利用 BST 的这个有序性，通过比较节点值来判断：\n如果 root 等于 p 或 q，直接返回 root 如果 root 的值在 p 和 q 之间，说明 p 和 q 分居两侧，也是直接返回 root 如果 root 小于 p 和 q，说明两者都在右子树，向右走 如果 root 大于 p 和 q，说明两者都在左子树，向左走 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 public static TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { // 从根节点开始向下遍历 while (root.val != p.val \u0026amp;\u0026amp; root.val != q.val) { // 如果 root 在 p 和 q 之间，说明找到了分叉点 if (Math.min(p.val, q.val) \u0026lt; root.val \u0026amp;\u0026amp; root.val \u0026lt; Math.max(p.val, q.val)) { break; } // p 和 q 都比 root 小，往左走；否则往右走 root = root.val \u0026lt; Math.min(p.val, q.val) ? root.right : root.left; } return root; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-05T20:14:15Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%80%E6%AC%A1%E9%81%8D%E5%8E%86%E6%90%9E%E5%AE%9A%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E7%9A%84%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88/","title":"一次遍历搞定二叉搜索树的最近公共祖先"},{"content":"\n核心思路 从根节点开始,向左右子树递归搜索目标节点,然后根据左右子树的返回结果判断最近公共祖先的位置。\n1.递归终止条件 1 2 3 if (root == null || root == p || root == q) { return root; } 如果搜索到了空节点,说明这条路径上没有找到目标节点；\n如果当前节点就是我们要找的节点之一,直接返回。\n2.向左右子树递归搜索 1 2 TreeNode l = lowestCommonAncestor(root.left, p, q); TreeNode r = lowestCommonAncestor(root.right, p, q); 分别在左子树和右子树中搜索 p 和 q。\n3.根据搜索结果判断 (1) p 和 q 分别在当前节点的左右两侧,那么当前节点 root 就是它们的最近公共祖先。\n1 2 3 if (l != null \u0026amp;\u0026amp; r != null) { return root; } (2)左右子树都返回 null,说明当前子树中既没有 p 也没有 q。\n1 2 3 if (l == null \u0026amp;\u0026amp; r == null) { return null; } (3)当 l 和 r 一个为空、一个不为空时,返回非空的那个结果。\n1 return l != null ? l : r; 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if (root == null || root == p || root == q) { // 遇到空,或者p,或者q,直接返回 return root; } TreeNode l = lowestCommonAncestor(root.left, p, q); TreeNode r = lowestCommonAncestor(root.right, p, q); if (l != null \u0026amp;\u0026amp; r != null) { // 左树也搜到,右树也搜到,返回root return root; } if (l == null \u0026amp;\u0026amp; r == null) { // 都没搜到返回空 return null; } // l和r一个为空,一个不为空 // 返回不空的那个 return l != null ? l : r; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-04T20:49:45Z","permalink":"https://iamxurulin.github.io/p/%E5%A6%82%E4%BD%95%E4%BC%98%E9%9B%85%E5%9C%B0%E6%89%BE%E5%88%B0%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88/","title":"如何优雅地找到二叉树的最近公共祖先"},{"content":"\n如果是普通二叉树，我们只能老老实实遍历每个节点,时间复杂度为O(N)。\n但完全二叉树有特殊的结构性质，利用完全二叉树的特性，我们可以将时间复杂度优化到O(logN × logN)。\n如果左子树是满二叉树，由满二叉树的节点数公式2^h - 1(h为高度)，可以直接算出左子树节点数，之后只需递归右子树。\n反之，如果右子树是满的，则左子树需要继续递归。\n代码逻辑 1. 主函数入口 1 2 3 4 5 6 public static int countNodes(TreeNode head) { if (head == null) { return 0; } return f(head, 1, mostLeft(head, 1)); } 2. 计算树的高度 1 2 3 4 5 6 7 public static int mostLeft(TreeNode cur, int level) { while (cur != null) { level++; cur = cur.left; } return level - 1; } 3. 递归函数 1 2 3 4 5 6 7 8 9 10 11 12 public static int f(TreeNode cur, int level, int h) { if (level == h) { return 1; } if (mostLeft(cur.right, level + 1) == h) { // 情况1:右子树的最左节点到达最深层 return (1 \u0026lt;\u0026lt; (h - level)) + f(cur.right, level + 1, h); } else { // 情况2:右子树的最左节点未到达最深层 return (1 \u0026lt;\u0026lt; (h - level - 1)) + f(cur.left, level + 1, h); } } 完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static int countNodes(TreeNode head) { if (head == null) { return 0; } return f(head, 1, mostLeft(head, 1)); } public static int f(TreeNode cur, int level, int h) { if (level == h) { return 1; } if (mostLeft(cur.right, level + 1) == h) { return (1 \u0026lt;\u0026lt; (h - level)) + f(cur.right, level + 1, h); } else { return (1 \u0026lt;\u0026lt; (h - level - 1)) + f(cur.left, level + 1, h); } } public static int mostLeft(TreeNode cur, int level) { while (cur != null) { level++; cur = cur.left; } return level - 1; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-03T20:30:29Z","permalink":"https://iamxurulin.github.io/p/%E5%A4%AA%E5%BC%BA%E4%BA%86%E8%BF%99%E4%B8%AA%E7%AE%97%E6%B3%95%E8%AE%A9%E9%9D%A2%E8%AF%95%E5%AE%98%E5%BD%93%E5%9C%BA%E6%83%8A%E5%91%BC-%E4%BD%A0%E6%98%AF%E6%80%8E%E4%B9%88%E6%83%B3%E5%88%B0%E7%9A%84/","title":"太强了!这个算法让面试官当场惊呼-你是怎么想到的"},{"content":"\n解题思路 用一个队列按层遍历所有节点，同时检测2个关键条件：\n1.不能出现\u0026quot;有右无左\u0026quot; 如果某个节点只有右孩子没有左孩子，直接判定为非完全二叉树。\n1 2 3 4 5 1 / \\ 2 3 \\ ← 节点2只有右孩子，违规！ 5 2.遇到\u0026quot;不双全\u0026quot;节点后，后续必须都是叶子节点 一旦遇到左右孩子不双全的节点（只有左孩子或两个都没有），那么后面的所有节点都必须是叶子节点。\n1 2 3 4 5 1 / \\ 2 3 / ← 节点2只有左孩子，后面的节点3必须是叶子 4 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public static int MAXN = 101; public static TreeNode[] queue = new TreeNode[MAXN]; public static int l, r; // l是队头指针，r是队尾指针 public static boolean isCompleteTree(TreeNode h) { if (h == null) { return true; // 空树认为是完全二叉树 } // 初始化队列 l = r = 0; queue[r++] = h; // leaf标记：是否遇到过\u0026#34;不双全\u0026#34;的节点 boolean leaf = false; while (l \u0026lt; r) { h = queue[l++]; // 出队 // 两种违规情况 if ((h.left == null \u0026amp;\u0026amp; h.right != null) || // 情况1：有右无左 (leaf \u0026amp;\u0026amp; (h.left != null || h.right != null))) { // 情况2：遇到leaf标记后还有孩子 return false; } // 左孩子入队 if (h.left != null) { queue[r++] = h.left; } // 右孩子入队 if (h.right != null) { queue[r++] = h.right; } // 如果当前节点不双全，标记leaf if (h.left == null || h.right == null) { leaf = true; } } return true; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-02T19:06:03Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%80%E9%81%93%E9%9D%A2%E8%AF%95%E9%AB%98%E9%A2%91%E9%A2%98-%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%AD%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91/","title":"一道面试高频题-如何判断完全二叉树"},{"content":" 这是一道二叉树的经典问题，不仅考察对二叉树遍历的理解，更考验递归思维和边界处理能力。\n核心思路 采用分治思想:\n通过前序遍历确定根节点,在中序遍历中定位该根节点的位置,以此为分界点划分出左右子树的范围,然后递归地对左右子树重复这个过程,最终完成整棵树的重建。\n索引计算的推导 假设:\n1 2 3 前序遍历: pre[l1 ... r1] 中序遍历: in[l2 ... r2] 根节点在中序中的位置: k 1.中序遍历的划分 根节点 k 天然就是分界点:\n1 2 中序: [l2 ... k-1] | k | [k+1 ... r2] ← 左子树 → 根 ← 右子树 → 左子树范围: in[l2, k-1] 右子树范围: in[k+1, r2] 2.前序遍历的划分 1 2 pre[l1] | pre[l1+1 ... ?] | pre[? ... r1] 根节点 左子树 右子树 已知左子树有 k - l2 个节点，所以:\n1 2 pre[l1] | pre[l1+1 ... l1+(k-l2)] | pre[l1+(k-l2)+1 ... r1] 根 左子树(k-l2个节点) 右子树 左子树范围: pre[l1+1, l1+k-l2] 右子树范围: pre[l1+k-l2+1, r1] 拿示例1举例 假设输入:\n1 2 前序遍历: [3, 9, 20, 15, 7] 中序遍历: [9, 3, 15, 20, 7] 第1步: 前序首元素 3 是根节点,在中序中位置为 1\n1 2 中序: [9] | 3 | [15, 20, 7] 左 根 右 第2步: 递归处理左子树 [9] 和右子树 [20, 15, 7]\n第3步: 右子树中,20 是根节点,继续划分\u0026hellip;\n最终构建出:\n1 2 3 4 5 3 / \\ 9 20 / \\ 15 7 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public static TreeNode buildTree(int[] pre, int[] in) { // 边界检查 if (pre == null || in == null || pre.length != in.length) { return null; } // 构建哈希表:值 → 在中序遍历中的索引 HashMap\u0026lt;Integer, Integer\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; in.length; i++) { map.put(in[i], i); } return f(pre, 0, pre.length - 1, in, 0, in.length - 1, map); } public static TreeNode f(int[] pre, int l1, int r1, int[] in, int l2, int r2, HashMap\u0026lt;Integer, Integer\u0026gt; map) { // 递归终止条件 if (l1 \u0026gt; r1) { return null; } // 创建根节点 TreeNode head = new TreeNode(pre[l1]); // 只有一个节点时直接返回 if (l1 == r1) { return head; } // 在中序遍历中找到根节点位置 int k = map.get(pre[l1]); // 递归构建左右子树 head.left = f(pre, l1 + 1, l1 + k - l2, in, l2, k - 1, map); head.right = f(pre, l1 + k - l2 + 1, r1, in, k + 1, r2, map); return head; } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-11-01T19:59:29Z","permalink":"https://iamxurulin.github.io/p/%E5%A4%A7%E5%8E%82%E9%9D%A2%E8%AF%95%E5%AE%98%E4%B8%8D%E4%BC%9A%E5%91%8A%E8%AF%89%E4%BD%A0-%E9%87%8D%E5%BB%BA%E4%BA%8C%E5%8F%89%E6%A0%91%E6%9C%89%E8%BF%99%E4%B8%AA%E6%8D%B7%E5%BE%84/","title":"大厂面试官不会告诉你-重建二叉树有这个捷径"},{"content":"安装好Docker启动之后报这个错误： 有博客说是因为没有启动【虚拟机平台】，于是我Win+r➡【optionalfeatures】，看了一下发现我的【Virtual Machine platform】是启动的，那到底是什么原因呢？\n问了一下AI，说这个错误表示 Windows 的 HCS (Host Compute Service) 服务不可用。\n执行了以下操作：\n1 2 3 4 5 6 7 8 9 10 11 12 # 1. 启用 vmcompute 服务 sc.exe config vmcompute start=auto # 2. 启动服务 sc.exe start vmcompute # 3. 启用 vmms sc.exe config vmms start=auto sc.exe start vmms # 4. 关闭 WSL wsl --shutdown 然后重新启动 Docker Desktop，出现这个报错： 这个错误表示虚拟化未启用。但是我打开任务管理器→ 切换到\u0026quot;性能\u0026quot;选项卡 → 点击\u0026quot;CPU\u0026quot;，底部显示 \u0026ldquo;虚拟化: 已启用\u0026rdquo;。\n接下来尝试查看 Hypervisor 状态，\n1 bcdedit /enum | findstr hypervisorlaunchtype 结果显示 off，需要启用一下：\n1 bcdedit /set hypervisorlaunchtype auto 重启电脑，然后再启动 Docker Desktop，问题就解决了。\n","date":"2025-10-31T22:36:04Z","permalink":"https://iamxurulin.github.io/p/an-error-occurred-while-running-a-wsl-command.please-check-your-wsl-configuration-and-try-again.%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95/","title":"An-error-occurred-while-running-a-WSL-command.Please-check-your-WSL-configuration-and-try-again.解决办法"},{"content":"\n在实际开发中，经常需要将二叉树保存到文件或通过网络传输。这就需要将树结构转换为字符串（序列化），以及从字符串恢复树结构（反序列化）。\n今天就来剖析这个序列化和反序列化的问题。\n🚀解题思路 🟦 用 前序遍历（根→左→右） 把树“拍扁”成字符串 🟦 用 # 表示空节点 🟦 反序列化时，递归按顺序读回来\n这样就能保证序列化和反序列化是一一对应的。\n💻 完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class Codec { // 序列化：将二叉树转成字符串 public String serialize(TreeNode root) { StringBuilder builder = new StringBuilder(); f(root, builder); return builder.toString(); } // 前序遍历构建字符串 void f(TreeNode root, StringBuilder builder) { if (root == null) { builder.append(\u0026#34;#,\u0026#34;); // 用#表示null节点 } else { builder.append(root.val + \u0026#34;,\u0026#34;); // 当前节点值 f(root.left, builder); // 递归左子树 f(root.right, builder); // 递归右子树 } } // 反序列化：将字符串还原为二叉树 public TreeNode deserialize(String data) { String[] vals = data.split(\u0026#34;,\u0026#34;); cnt = 0; return g(vals); } // 当前解析到数组的第几个元素 public static int cnt; // 递归构建树 TreeNode g(String[] vals) { String cur = vals[cnt++]; if (cur.equals(\u0026#34;#\u0026#34;)) { return null; // 空节点 } else { TreeNode head = new TreeNode(Integer.valueOf(cur)); head.left = g(vals); head.right = g(vals); return head; } } } 如果觉得有帮助，欢迎点赞、关注、转发~\n","date":"2025-10-31T21:58:22Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%8F%89%E6%A0%91%E5%BA%8F%E5%88%97%E5%8C%96%E4%B8%8E%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/","title":"二叉树序列化与反序列化"},{"content":"二叉树的最大深度 代码逻辑 当遇到空节点时返回深度 0 作为递归的终止条件，否则分别递归计算左右子树的深度，取两者中的较大值后加 1（这个 +1 代表当前节点本身占据的一层），最终得到以当前节点为根的子树的最大深度。\n代码实现\n1 2 3 public static int maxDepth(TreeNode root) { return root == null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; } 二叉树的最小深度 代码逻辑 代码通过三层判断来确保找到真正的最小深度：\n首先判断空树直接返回 0，其次判断叶子节点（左右子树都为空）直接返回 1，最后对于有子树的节点，将左右深度初始化为 Integer.MAX_VALUE，只有当某侧子树存在时才递归计算其真实深度，这样在取 min 时会自动忽略不存在的子树（因为 MAX_VALUE 必然更大），最终加 1 得到当前节点的最小深度。\n代码实现\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public int minDepth(TreeNode root) { if (root == null) { // 空树深度为 0 return 0; } if (root.left == null \u0026amp;\u0026amp; root.right == null) { // 当前节点是叶子节点 return 1; } int ldeep = Integer.MAX_VALUE; int rdeep = Integer.MAX_VALUE; if (root.left != null) { ldeep = minDepth(root.left); } if (root.right != null) { rdeep = minDepth(root.right); } return Math.min(ldeep, rdeep) + 1; } ","date":"2025-10-30T20:51:38Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%8F%89%E6%A0%91%E6%9C%80%E5%A4%A7%E6%B7%B1%E5%BA%A6%E4%B8%8E%E6%9C%80%E5%B0%8F%E6%B7%B1%E5%BA%A6-%E4%B8%80%E6%96%87%E6%90%9E%E6%87%82%E9%80%92%E5%BD%92%E7%9A%84%E7%B2%BE%E9%AB%93%E4%B8%8E%E9%99%B7%E9%98%B1/","title":"二叉树最大深度与最小深度-一文搞懂递归的精髓与陷阱"},{"content":"\n代码逻辑 这道题就是给每个节点编号，根节点是1，然后左子节点就是父节点编号乘2，右子节点是父节点编号乘2再加1。\n层序遍历的时候，每一层的宽度就等于这层最右边的编号减去最左边的编号再加1，把所有层遍历一遍取个最大值就行了。\n代码里用 nq[] 存节点，iq[] 存对应的编号，l 和 r 两个指针来模拟队列操作。\n完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public static int MAXN = 3001; public static TreeNode[] nq = new TreeNode[MAXN]; // 节点队列 public static int[] iq = new int[MAXN]; // 编号队列 public static int l, r; // 队列的左右指针 public static int widthOfBinaryTree(TreeNode root) { int ans = 1; l = r = 0; // 初始化：根节点入队，编号为1 nq[r] = root; iq[r++] = 1; while (l \u0026lt; r) { int size = r - l; // 当前层的节点数量 // 计算当前层的宽度 ans = Math.max(ans, iq[r - 1] - iq[l] + 1); // 处理当前层的所有节点 for (int i = 0; i \u0026lt; size; i++) { TreeNode node = nq[l]; int id = iq[l++]; // 左子节点入队 if (node.left != null) { nq[r] = node.left; iq[r++] = id * 2; } // 右子节点入队 if (node.right != null) { nq[r] = node.right; iq[r++] = id * 2 + 1; } } } return ans; } ","date":"2025-10-29T20:25:06Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%8F%89%E6%A0%91%E6%9C%80%E5%A4%A7%E5%AE%BD%E5%BA%A6-%E5%AD%A6%E4%BC%9A%E8%BF%99%E4%B8%AA%E6%8A%80%E5%B7%A7-%E7%B1%BB%E4%BC%BC%E9%A2%98%E7%9B%AE%E7%A7%92%E6%9D%80/","title":"二叉树最大宽度-学会这个技巧-类似题目秒杀"},{"content":"\n代码逻辑 这道题如果用队列（Queue）来做层序遍历，然后每层根据奇偶性决定是正序还是反序。\n这样操作会遇到一些麻烦，比如用双端队列（Deque），需要频繁在两端操作；如果用普通队列，收集结果时需要反转列表。\n今天分享一种解法：\n用一个静态数组模拟队列，通过索引控制实现锯齿形遍历。\n1. 用数组模拟队列 1 2 public static TreeNode[] queue = new TreeNode[MAXN]; public static int l, r; // l是队头，r是队尾 2. 双循环结构 第一个循环：收集当前层的节点值\n1 2 3 4 5 6 for (int i = reverse ? r - 1 : l, j = reverse ? -1 : 1, k = 0; k \u0026lt; size; i += j, k++) { TreeNode cur = queue[i]; list.add(cur.val); } 如果 reverse == false（从左到右）：i 从 l 开始，每次 +1；\n如果 reverse == true（从右到左）：i 从 r-1 开始，每次 -1；\n第二个循环：扩展下一层节点\n1 2 3 4 5 6 7 8 9 for (int i = 0; i \u0026lt; size; i++) { TreeNode cur = queue[l++]; // 从队头取出 if (cur.left != null) { queue[r++] = cur.left; // 放入队尾 } if (cur.right != null) { queue[r++] = cur.right; } } 按照左到右的顺序处理节点，把下一层的节点依次加入队列。\n完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public static int MAXN = 2001; public static TreeNode[] queue = new TreeNode[MAXN]; public static int l, r; // l是队头，r是队尾 public static List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; zigzagLevelOrder(TreeNode root){ List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); if(root!=null){ l=r=0; queue[r++]=root; boolean reverse = false; while(l\u0026lt;r){ int size = r-l; ArrayList\u0026lt;Integer\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); for(int i=reverse?r-1:l,j=reverse?-1:1,k=0;k\u0026lt;size;i+=j,k++){ TreeNode cur = queue[i]; list.add(cur.val); } for (int i = 0; i \u0026lt; size; i++) { TreeNode cur = queue[l++]; // 从队头取出 if (cur.left != null) { queue[r++] = cur.left; // 放入队尾 } if (cur.right != null) { queue[r++] = cur.right; } } ans.add(list); reverse = !reverse; } } return ans; } ","date":"2025-10-28T20:45:37Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E9%94%AF%E9%BD%BF%E5%BD%A2%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86/","title":"二叉树的锯齿形层序遍历"},{"content":"\n今天来聊聊二叉树的层序遍历问题。这道题在面试中的出镜率极高,是 BFS(广度优先搜索)的经典应用。\n方法1:HashMap 记录层级 代码逻辑 核心是用 HashMap 记录每个节点所在的层级。\n先将根节点入队并标记为第 0 层,然后逐个出队处理节点,根据 HashMap 中的层级信息将节点值放入对应层的列表中,同时将子节点入队并标记为下一层。这样通过 HashMap 作为\u0026quot;层级标签\u0026quot;,让每个节点都能准确找到自己该归属的那一层。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public static List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; levelOrder1(TreeNode root) { List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); if (root != null) { Queue\u0026lt;TreeNode\u0026gt; queue = new LinkedList\u0026lt;\u0026gt;(); HashMap\u0026lt;TreeNode, Integer\u0026gt; levels = new HashMap\u0026lt;\u0026gt;(); queue.add(root); levels.put(root, 0); // 根节点在第0层 while (!queue.isEmpty()) { TreeNode cur = queue.poll(); int level = levels.get(cur); // 如果当前层还没有列表,就创建一个 if (ans.size() == level) { ans.add(new ArrayList\u0026lt;\u0026gt;()); } // 将当前节点加入对应层 ans.get(level).add(cur.val); // 将子节点加入队列,并记录它们的层级 if (cur.left != null) { queue.add(cur.left); levels.put(cur.left, level + 1); } if (cur.right != null) { queue.add(cur.right); levels.put(cur.right, level + 1); } } } return ans; } 方法2:按层处理优化 代码逻辑 在每轮循环开始时,先用 size = r - l 记录当前队列中有多少个节点(即当前层的节点数),然后用 for 循环恰好处理这 size 个节点,在出队收集节点值的同时将下一层节点入队,这样每轮循环恰好完成一层的遍历。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public static int MAXN = 2001; public static TreeNode[] queue = new TreeNode[MAXN]; public static int l, r; // l:队列头指针, r:队列尾指针 public static List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; levelOrder2(TreeNode root) { List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); if (root != null) { l = r = 0; queue[r++] = root; while (l \u0026lt; r) { int size = r - l; // 当前层的节点数量 ArrayList\u0026lt;Integer\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); // 处理当前层的所有节点 for (int i = 0; i \u0026lt; size; i++) { TreeNode cur = queue[l++]; list.add(cur.val); if (cur.left != null) { queue[r++] = cur.left; } if (cur.right != null) { queue[r++] = cur.right; } } ans.add(list); } } return ans; } ","date":"2025-10-27T21:50:37Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%8F%89%E6%A0%91%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86-90%E7%9A%84%E4%BA%BA%E9%83%BD%E7%94%A8%E9%94%99%E4%BA%86%E6%96%B9%E6%B3%95/","title":"二叉树层序遍历-90%的人都用错了方法！"},{"content":" 这是一个非常有意思的数据结构设计题，要求我们设计一个支持所有操作的时间复杂度都必须是 O(1)的数据结构。\n代码逻辑 这道题的难点在于:\n如何在 O(1) 时间内既能修改计数,又能快速找到最大/最小值?\n如果只用 HashMap 存储计数,修改很快,但找最大/最小值需要遍历,时间复杂度是 O(n)。\n如果用排序结构,查找最大/最小值很快,但插入删除的时间复杂度又无法保证 O(1)。\n为了解决这个难题，我的思路是用一个双向链表按计数大小有序地存储所有\u0026quot;桶\u0026quot;(Bucket)，其中每个桶代表一个特定的计数值,包含所有该计数的 key，再用一个 HashMap 记录每个 key 当前所在的桶。\n1. Bucket(桶)结构 1 2 3 4 5 6 7 8 9 10 11 12 class Bucket { public HashSet\u0026lt;String\u0026gt; set; // 存储所有该计数的 key public int cnt; // 计数值 public Bucket last; // 前驱节点 public Bucket next; // 后继节点 public Bucket(String s, int c) { set = new HashSet\u0026lt;\u0026gt;(); set.add(s); cnt = c; } } 2. inc(key) - 增加计数 如果key 不存在，需要创建计数为 1 的桶；如果 head.next 的计数正好是 1,直接加入该桶，否则,在 head 后创建新桶。\n如果key 已存在，从当前桶移动到下一个计数的桶，如果 next 的计数正好是 cnt+1,直接加入，否则,创建新桶插入当前桶后面。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 if (!map.containsKey(key)) { if (head.next.cnt == 1) { map.put(key, head.next); head.next.set.add(key); } else { Bucket newBucket = new Bucket(key, 1); map.put(key, newBucket); insert(head, newBucket); } }else { Bucket bucket = map.get(key); if (bucket.next.cnt == bucket.cnt + 1) { map.put(key, bucket.next); bucket.next.set.add(key); } else { Bucket newBucket = new Bucket(key, bucket.cnt + 1); map.put(key, newBucket); insert(bucket, newBucket); } bucket.set.remove(key); if (bucket.set.isEmpty()) { remove(bucket); // 如果桶空了,删除该桶 } } 3. dec(key) - 减少计数 如果计数减到 0,需要从 map 中删除该 key，否则,向前移动到计数减 1 的桶。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void dec(String key) { Bucket bucket = map.get(key); if (bucket.cnt == 1) { map.remove(key); // 计数为0,彻底删除 } else { if (bucket.last.cnt == bucket.cnt - 1) { map.put(key, bucket.last); bucket.last.set.add(key); } else { Bucket newBucket = new Bucket(key, bucket.cnt - 1); map.put(key, newBucket); insert(bucket.last, newBucket); } } bucket.set.remove(key); if (bucket.set.isEmpty()) { remove(bucket); } } 4. getMaxKey() 和 getMinKey() 1 2 3 4 5 6 7 public String getMaxKey() { return tail.last.set.iterator().next(); } public String getMinKey() { return head.next.set.iterator().next(); } 5.insert() - 在指定位置插入桶 1 2 3 4 5 6 private void insert(Bucket cur, Bucket pos) { cur.next.last = pos; pos.next = cur.next; cur.next = pos; pos.last = cur; } 6.remove() - 删除桶 1 2 3 4 private void remove(Bucket cur) { cur.last.next = cur.next; cur.next.last = cur.last; } 完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 class AllOne { class Bucket { public HashSet\u0026lt;String\u0026gt; set; public int cnt; public Bucket last; public Bucket next; public Bucket(String s, int c) { set = new HashSet\u0026lt;\u0026gt;(); set.add(s); cnt = c; } } private void insert(Bucket cur, Bucket pos) { cur.next.last = pos; pos.next = cur.next; cur.next = pos; pos.last = cur; } private void remove(Bucket cur) { cur.last.next = cur.next; cur.next.last = cur.last; } Bucket head; // 哨兵头节点(cnt=0) Bucket tail; // 哨兵尾节点(cnt=MAX) HashMap\u0026lt;String, Bucket\u0026gt; map; // key -\u0026gt; 所在桶的映射 public AllOne() { head = new Bucket(\u0026#34;\u0026#34;,0); tail = new Bucket(\u0026#34;\u0026#34;,Integer.MAX_VALUE); head.next = tail; tail.last = head; map = new HashMap\u0026lt;\u0026gt;(); } public void inc(String key){ if(!map.containsKey(key)){ if(head.next.cnt==1){ map.put(key,head.next); head.next.set.add(key); }else { Bucket newBucket = new Bucket(key,1); map.put(key,newBucket); insert(head,newBucket); } }else{ Bucket bucket = map.get(key); if(bucket.next.cnt==bucket.cnt+1){ map.put(key,bucket.next); bucket.next.set.add(key); }else{ Bucket newBucket = new Bucket(key, bucket.cnt+1); map.put(key,newBucket); insert(bucket,newBucket); } bucket.set.remove(key); if(bucket.set.isEmpty()){ remove(bucket);// 如果桶空了,删除该桶 } } } public void dec(String key){ Bucket bucket = map.get(key); if(bucket.cnt==1){ map.remove(key);// 计数为0,彻底删除 }else{ if(bucket.last.cnt==bucket.cnt-1){ map.put(key,bucket.last); bucket.last.set.add(key); }else{ Bucket newBucket = new Bucket(key,bucket.cnt-1); map.put(key,newBucket); insert(bucket.last,newBucket); } } bucket.set.remove(key); if(bucket.set.isEmpty()){ remove(bucket); } } public String getMaxKey(){ return tail.last.set.iterator().next(); } public String getMinKey(){ return head.next.set.iterator().next(); } } 如果觉得有帮助,欢迎点赞关注! 🌟\n","date":"2025-10-26T21:23:06Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E5%AE%98-%E8%AE%BE%E8%AE%A1%E4%B8%80%E4%B8%AA%E5%85%A8o1%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BD%A0%E4%BC%9A%E5%90%97/","title":"面试官-设计一个全O(1)的数据结构,你会吗"},{"content":"网上有很多免费的在线图床能够做到快速上传图片并获取URL。\n但是作为一个纯粹的技术人，我还是想通过专业一点的手法实现这样一个功能。\n利用GitHub的公共仓库存储图片，再通过仓库的文件原始链接（Raw URL）获取图片地址，能够免费无限存储、URL永久有效。\n下面介绍采用Edge浏览器在GitHub网页端进行操作。\n操作步骤 1.在 GitHub官网 免费注册一个账号，并登录。\n2.创建一个公共仓库，因为GitHub的私有仓库文件URL需要权限验证，无法直接公开访问，所以这里必须创建公共仓库。\n「Repository name」：取一个名字（例如picture-storage）。 「Description」：比如“图片存储库”。 勾选「Public」（必须公开，否则图片URL无法直接访问）。 勾选「Add a README file」（方便管理）。 3.进入创建的仓库（例如 https://github.com/你的用户名/picture-storage），上传图片： 点击「+」图标，选择「Upload files」 点击「choose your files」选择本地的图片进行上传 选择好图片后在「Commit changes」区域填写备注（例如“上传头像图片”），然后点击「Commit changes」确认上传。 4.上传后，图片会保存在仓库中，由于普通预览链接无法直接显示图片，需要获取其“原始文件链接”（Raw URL）。\n在仓库中找到刚上传的图片，点击图片名称进入预览页面（例如 https://github.com/你的用户名/picture-storage/blob/main/xxx.png）； 在图片预览页面，右键点击图片，选择「在新标签页中打开图像」； 在新的标签页地址栏的URL就是Raw链接（例如 https://raw.githubusercontent.com/你的用户名/picture-storage/main/xxx.png），直接复制即可。 通过以上步骤，即可快速通过GitHub上传图片并获取可用的URL，适用于Markdown文档、个人博客、社交平台等场景。\n","date":"2025-10-26T11:16:02Z","permalink":"https://iamxurulin.github.io/p/github%E4%BB%93%E5%BA%93%E4%B8%8A%E4%BC%A0%E5%9B%BE%E7%89%87%E5%B9%B6%E8%8E%B7%E5%8F%96url/","title":"Github仓库上传图片并获取URL"},{"content":"\n代码逻辑 这道题的关键在于：如何同时维护频率信息和时间顺序？\n我们可以把栈想象成一个\u0026quot;蛋糕\u0026quot;，按频率分层：\n第 1 层：存放所有出现过 1 次的元素 第 2 层：存放所有出现过 2 次的元素 第 3 层：存放所有出现过 3 次的元素 \u0026hellip; 每当一个元素被 push 进来，它的频率加 1然后它会被添加到对应频率的那一层。\n每当执行 pop 操作，从最高层（最大频率层）取出最后一个元素，如果该层变空，层数减 1。\n1.数据结构设计 1 2 3 4 5 6 7 8 9 10 class FreqStack { // 当前最大频率 private int topTimes; // 分层存储：频率 -\u0026gt; 该频率下的元素列表 private HashMap\u0026lt;Integer, ArrayList\u0026lt;Integer\u0026gt;\u0026gt; cntValues= new HashMap\u0026lt;\u0026gt;(); // 元素计数：元素值 -\u0026gt; 出现次数 private HashMap\u0026lt;Integer, Integer\u0026gt; valueTimes== new HashMap\u0026lt;\u0026gt;(); } 2.push 操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void push(int val) { // 更新该元素的频率 valueTimes.put(val, valueTimes.getOrDefault(val, 0) + 1); int curTopTimes = valueTimes.get(val); // 将元素添加到对应频率层 if (!cntValues.containsKey(curTopTimes)) { cntValues.put(curTopTimes, new ArrayList\u0026lt;\u0026gt;()); } ArrayList\u0026lt;Integer\u0026gt; curTimeValues = cntValues.get(curTopTimes); curTimeValues.add(val); // 更新最大频率 topTimes = Math.max(topTimes, curTopTimes); } 3.pop 操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public int pop() { // 从最高层取出最后一个元素 ArrayList\u0026lt;Integer\u0026gt; topTimeValues = cntValues.get(topTimes); int ans = topTimeValues.remove(topTimeValues.size() - 1); // 如果该层变空，删除该层并降低最高频率 if (topTimeValues.size() == 0) { cntValues.remove(topTimes--); } // 更新元素的频率计数 int times = valueTimes.get(ans); if (times == 1) { valueTimes.remove(ans); } else { valueTimes.put(ans, times - 1); } return ans; } 完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 class FreqStack{ // 当前最大频率 private int topTimes; // 分层存储：频率 -\u0026gt; 该频率下的元素列表 private HashMap\u0026lt;Integer, ArrayList\u0026lt;Integer\u0026gt;\u0026gt; cntValues= new HashMap\u0026lt;\u0026gt;(); // 元素计数：元素值 -\u0026gt; 出现次数 private HashMap\u0026lt;Integer, Integer\u0026gt; valueTimes= new HashMap\u0026lt;\u0026gt;(); public void push(int val) { // 更新该元素的频率 valueTimes.put(val, valueTimes.getOrDefault(val, 0) + 1); int curTopTimes = valueTimes.get(val); // 将元素添加到对应频率层 if (!cntValues.containsKey(curTopTimes)) { cntValues.put(curTopTimes, new ArrayList\u0026lt;\u0026gt;()); } ArrayList\u0026lt;Integer\u0026gt; curTimeValues = cntValues.get(curTopTimes); curTimeValues.add(val); // 更新最大频率 topTimes = Math.max(topTimes, curTopTimes); } public int pop() { // 从最高层取出最后一个元素 ArrayList\u0026lt;Integer\u0026gt; topTimeValues = cntValues.get(topTimes); int ans = topTimeValues.remove(topTimeValues.size() - 1); // 如果该层变空，删除该层并降低最高频率 if (topTimeValues.size() == 0) { cntValues.remove(topTimes--); } // 更新元素的频率计数 int times = valueTimes.get(ans); if (times == 1) { valueTimes.remove(ans); } else { valueTimes.put(ans, times - 1); } return ans; } } 关注我，带你深度剖析更多经典算法题！ 🚀\n","date":"2025-10-25T19:52:07Z","permalink":"https://iamxurulin.github.io/p/%E6%9C%80%E5%A4%A7%E9%A2%91%E7%8E%87%E6%A0%88/","title":"最大频率栈"},{"content":"\n代码逻辑 这道题的关键在于如何在动态添加元素的过程中,快速找到中位数。\n我们可以用一个大顶堆(maxHeap)存储较小的一半元素，堆顶是这部分的最大值和一个小顶堆(minHeap)存储较大的一半元素,堆顶是这部分的最小值，这样的话，中位数就在两个堆的堆顶附近!\n1.添加元素 添加新元素时， 如果大顶堆为空,或新元素 ≤ 大顶堆堆顶 → 放入大顶堆，否则 → 放入小顶堆。 如果两个堆的大小差为 2,就从元素多的堆中取出堆顶,放入另一个堆。\n2.查找中位数 如果两个堆大小相等 → 中位数 = (大顶堆堆顶 + 小顶堆堆顶) / 2;\n如果大小不等 → 中位数 = 元素多的那个堆的堆顶。\n代码实现\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 class MedianFinder { private PriorityQueue\u0026lt;Integer\u0026gt; maxHeap; // 大顶堆,存储较小的一半 private PriorityQueue\u0026lt;Integer\u0026gt; minHeap; // 小顶堆,存储较大的一半 public MedianFinder() { // Java的PriorityQueue默认是小顶堆,需要自定义比较器实现大顶堆 maxHeap = new PriorityQueue\u0026lt;\u0026gt;((a, b) -\u0026gt; b - a); minHeap = new PriorityQueue\u0026lt;\u0026gt;((a, b) -\u0026gt; a - b); } public void addNum(int num) { // 决定新元素放入哪个堆 if (maxHeap.isEmpty() || maxHeap.peek() \u0026gt;= num) { maxHeap.add(num); } else { minHeap.add(num); } // 调整两个堆的平衡 balance(); } public double findMedian() { if (maxHeap.size() == minHeap.size()) { // 偶数个元素,返回两个堆顶的平均值 return (double) (maxHeap.peek() + minHeap.peek()) / 2; } else { // 奇数个元素,返回元素多的那个堆的堆顶 return maxHeap.size() \u0026gt; minHeap.size() ? maxHeap.peek() : minHeap.peek(); } } private void balance() { // 如果两个堆的大小差为2,需要调整 if (Math.abs(maxHeap.size() - minHeap.size()) == 2) { if (maxHeap.size() \u0026gt; minHeap.size()) { minHeap.add(maxHeap.poll()); } else { maxHeap.add(minHeap.poll()); } } } } ","date":"2025-10-24T20:34:55Z","permalink":"https://iamxurulin.github.io/p/%E5%8F%8C%E5%A0%86%E6%B3%95%E6%B1%82%E6%95%B0%E6%8D%AE%E6%B5%81%E7%9A%84%E4%B8%AD%E4%BD%8D%E6%95%B0/","title":"双堆法求数据流的中位数"},{"content":"\n与Leetcode380不同的是，这道题允许元素重复。\n代码逻辑 1. 插入操作 1 2 3 4 5 6 7 public boolean insert(int val) { arr.add(val); // 直接添加到数组末尾 HashSet\u0026lt;Integer\u0026gt; set = map.getOrDefault(val, new HashSet\u0026lt;Integer\u0026gt;()); set.add(arr.size() - 1); // 记录该元素在数组中的索引 map.put(val, set); return set.size() == 1; // 如果是第一次插入,返回 true } 将元素添加到数组末尾,同时在 HashMap 中记录其索引位置。\n2. 删除操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public boolean remove(int val) { if (!map.containsKey(val)) { return false; } // 获取要删除元素的任意一个索引 HashSet\u0026lt;Integer\u0026gt; valSet = map.get(val); int valAnyIndex = valSet.iterator().next(); // 获取数组末尾元素 int endValue = arr.get(arr.size() - 1); if (val == endValue) { // 特殊情况:要删除的就是末尾元素 valSet.remove(arr.size() - 1); } else { // 将末尾元素移动到要删除的位置 HashSet\u0026lt;Integer\u0026gt; endValueSet = map.get(endValue); endValueSet.add(valAnyIndex); // 末尾元素有了新位置 arr.set(valAnyIndex, endValue); // 更新数组 endValueSet.remove(arr.size() - 1); // 移除末尾位置 valSet.remove(valAnyIndex); // 移除被删除元素的位置 } arr.remove(arr.size() - 1); // 删除数组末尾 if (valSet.isEmpty()) { map.remove(val); // 如果该值已经没有了,从 map 中删除 } return true; } ArrayList 删除中间元素是 O(n) 操作,但删除末尾元素是 O(1)，因此我们把要删除的元素和末尾元素交换,然后删除末尾。\n3. 随机获取 1 2 3 public int getRandom() { return arr.get((int) (Math.random() * arr.size())); } 完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 class RandomizedCollection{ public HashMap\u0026lt;Integer,HashSet\u0026lt;Integer\u0026gt;\u0026gt; map; public ArrayList\u0026lt;Integer\u0026gt; arr; public RandomizedCollection(){ map = new HashMap\u0026lt;\u0026gt;(); arr = new ArrayList\u0026lt;\u0026gt;(); } public boolean insert(int val) { arr.add(val); // 直接添加到数组末尾 HashSet\u0026lt;Integer\u0026gt; set = map.getOrDefault(val, new HashSet\u0026lt;Integer\u0026gt;()); set.add(arr.size() - 1); // 记录该元素在数组中的索引 map.put(val, set); return set.size() == 1; // 如果是第一次插入,返回 true } public boolean remove(int val) { if (!map.containsKey(val)) { return false; } // 获取要删除元素的任意一个索引 HashSet\u0026lt;Integer\u0026gt; valSet = map.get(val); int valAnyIndex = valSet.iterator().next(); // 获取数组末尾元素 int endValue = arr.get(arr.size() - 1); if (val == endValue) { // 特殊情况:要删除的就是末尾元素 valSet.remove(arr.size() - 1); } else { // 将末尾元素移动到要删除的位置 HashSet\u0026lt;Integer\u0026gt; endValueSet = map.get(endValue); endValueSet.add(valAnyIndex); // 末尾元素有了新位置 arr.set(valAnyIndex, endValue); // 更新数组 endValueSet.remove(arr.size() - 1); // 移除末尾位置 valSet.remove(valAnyIndex); // 移除被删除元素的位置 } arr.remove(arr.size() - 1); // 删除数组末尾 if (valSet.isEmpty()) { map.remove(val); // 如果该值已经没有了,从 map 中删除 } return true; } public int getRandom(){ return arr.get((int) (Math.random()*arr.size())); } } ","date":"2025-10-23T20:48:13Z","permalink":"https://iamxurulin.github.io/p/leetcode-381-o1-%E6%97%B6%E9%97%B4%E6%8F%92%E5%85%A5%E5%88%A0%E9%99%A4%E5%92%8C%E8%8E%B7%E5%8F%96%E9%9A%8F%E6%9C%BA%E5%85%83%E7%B4%A0-%E5%85%81%E8%AE%B8%E9%87%8D%E5%A4%8D/","title":"LeetCode-381-O(1)-时间插入、删除和获取随机元素-允许重复"},{"content":" 这道题的难点在于如何同时满足三个 O(1) 操作,尤其是随机获取元素。\n代码逻辑 1. 插入操作 (insert) 1 2 3 4 5 6 7 8 public boolean insert(int val) { if (map.containsKey(val)) { return false; // 元素已存在 } map.put(val, arr.size()); // 记录新元素的索引 arr.add(val); // 添加到数组末尾 return true; } 2. 删除操作 (remove) 直接从数组中间删除元素的时间复杂度是 O(n),因此本文采用交换删除法:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public boolean remove(int val) { if (!map.containsKey(val)) { return false; // 元素不存在 } // 获取要删除元素的索引 int valIndex = map.get(val); // 获取数组最后一个元素 int endValue = arr.get(arr.size() - 1); // 用最后一个元素覆盖要删除的元素 map.put(endValue, valIndex); arr.set(valIndex, endValue); // 删除 map 中的记录和数组末尾元素 map.remove(val); arr.remove(arr.size() - 1); return true; } 将要删除的元素与数组最后一个元素交换,然后删除末尾元素,这样删除操作就是 O(1) 了!\n3. 随机获取 (getRandom) 1 2 3 public int getRandom() { return arr.get((int) (Math.random() * arr.size())); } 使用了 ArrayList,可以通过索引 O(1) 访问任意元素,配合随机数生成器即可实现等概率随机返回。\n完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class RandomizedSet{ public HashMap\u0026lt;Integer,Integer\u0026gt; map; public ArrayList\u0026lt;Integer\u0026gt; arr; public RandomizedSet(){ map = new HashMap\u0026lt;\u0026gt;(); arr = new ArrayList\u0026lt;\u0026gt;(); } public boolean insert(int val) { if (map.containsKey(val)) { return false; // 元素已存在 } map.put(val, arr.size()); // 记录新元素的索引 arr.add(val); // 添加到数组末尾 return true; } public boolean remove(int val) { if (!map.containsKey(val)) { return false; // 元素不存在 } // 获取要删除元素的索引 int valIndex = map.get(val); // 获取数组最后一个元素 int endValue = arr.get(arr.size() - 1); // 用最后一个元素覆盖要删除的元素 map.put(endValue, valIndex); arr.set(valIndex, endValue); // 删除 map 中的记录和数组末尾元素 map.remove(val); arr.remove(arr.size() - 1); return true; } public int getRandom(){ return arr.get((int) (Math.random()*arr.size())); } } ","date":"2025-10-22T21:10:40Z","permalink":"https://iamxurulin.github.io/p/leetcode-380-o1-%E6%97%B6%E9%97%B4%E6%8F%92%E5%85%A5%E5%88%A0%E9%99%A4%E5%92%8C%E8%8E%B7%E5%8F%96%E9%9A%8F%E6%9C%BA%E5%85%83%E7%B4%A0/","title":"LeetCode-380-O(1)-时间插入、删除和获取随机元素"},{"content":"\nLRU的核心思想是当缓存空间满了,优先淘汰最久未被使用的数据。\n这种思想其实在生活中也有，比如想象一下你的书桌只能放5本书。每次看完一本书,你就把它放在最右边。当需要放新书但桌面满了时,你会扔掉最左边那本(最久没看的)。这其实就是LRU的思想!\n代码逻辑 1. addNode 1 2 3 4 5 6 7 8 9 10 11 12 public void addNode(DoubleNode newNode) { if (head == null) { // 链表为空,新节点既是头也是尾 head = newNode; tail = newNode; } else { // 追加到尾部 tail.next = newNode; newNode.last = tail; tail = newNode; } } 2. moveNodeToTail 每次访问一个节点,都要把它移到尾部,表示\u0026quot;最近刚用过\u0026quot;。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public void moveNodeToTail(DoubleNode node) { if (tail == node) return; // 已经在尾部 // 从原位置摘除节点 if (head == node) { head = node.next; head.last = null; } else { node.last.next = node.next; node.next.last = node.last; } // 追加到尾部 node.last = tail; node.next = null; tail.next = node; tail = node; } 3. removeHead 当缓存满了需要淘汰数据时,删除头部(最久未使用的)节点。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public DoubleNode removeHead() { if (head == null) return null; DoubleNode ans = head; if (head == tail) { // 只有一个节点 head = null; tail = null; } else { head = ans.next; ans.next = null; head.last = null; } return ans; } 4.get - 查询数据 1 2 3 4 5 6 7 8 public int get(int key) { if (keyNodeMap.containsKey(key)) { DoubleNode ans = keyNodeMap.get(key); nodeList.moveNodeToTail(ans); // 标记为最近使用 return ans.val; } return -1; } 5.put - 插入或更新数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void put(int key, int value) { if (keyNodeMap.containsKey(key)) { // key已存在,更新value DoubleNode node = keyNodeMap.get(key); node.val = value; nodeList.moveNodeToTail(node); // 移到尾部 } else { // key不存在,插入新节点 if (keyNodeMap.size() == capacity) { // 缓存已满,先淘汰最久未使用的 DoubleNode removed = nodeList.removeHead(); keyNodeMap.remove(removed.key); } DoubleNode newNode = new DoubleNode(key, value); keyNodeMap.put(key, newNode); nodeList.addNode(newNode); } } 完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 class LRUCache{ class DoubleNode { public int key; // 键 public int val; // 值 public DoubleNode last; // 前驱节点 public DoubleNode next; // 后继节点 public DoubleNode(int k, int v) { key = k; val = v; } } class DoubleList{ private DoubleNode head; private DoubleNode tail; public DoubleList(){ head = null; tail = null; } public void addNode(DoubleNode newNode) { if (head == null) { // 链表为空,新节点既是头也是尾 head = newNode; tail = newNode; } else { // 追加到尾部 tail.next = newNode; newNode.last = tail; tail = newNode; } } public void moveNodeToTail(DoubleNode node) { if (tail == node) return; // 已经在尾部 // 第一步: 从原位置摘除节点 if (head == node) { head = node.next; head.last = null; } else { node.last.next = node.next; node.next.last = node.last; } // 第二步: 追加到尾部 node.last = tail; node.next = null; tail.next = node; tail = node; } public DoubleNode removeHead() { if (head == null) return null; DoubleNode ans = head; if (head == tail) { // 只有一个节点 head = null; tail = null; } else { head = ans.next; ans.next = null; head.last = null; } return ans; } } private HashMap\u0026lt;Integer, DoubleNode\u0026gt; keyNodeMap; private DoubleList nodeList; private final int capacity; public LRUCache(int cap){ keyNodeMap = new HashMap\u0026lt;\u0026gt;(); nodeList = new DoubleList(); capacity = cap; } public int get(int key) { if (keyNodeMap.containsKey(key)) { DoubleNode ans = keyNodeMap.get(key); nodeList.moveNodeToTail(ans); // 标记为最近使用 return ans.val; } return -1; } public void put(int key, int value) { if (keyNodeMap.containsKey(key)) { // key已存在,更新value DoubleNode node = keyNodeMap.get(key); node.val = value; nodeList.moveNodeToTail(node); // 移到尾部 } else { // key不存在,插入新节点 if (keyNodeMap.size() == capacity) { // 缓存已满,先淘汰最久未使用的 DoubleNode removed = nodeList.removeHead(); keyNodeMap.remove(removed.key); } DoubleNode newNode = new DoubleNode(key, value); keyNodeMap.put(key, newNode); nodeList.addNode(newNode); } } } 关注我,持续分享算法干货! 🚀\n","date":"2025-10-21T21:52:10Z","permalink":"https://iamxurulin.github.io/p/lru%E7%BC%93%E5%AD%98%E6%B7%98%E6%B1%B0%E7%AE%97%E6%B3%95java%E5%AE%9E%E7%8E%B0/","title":"LRU缓存淘汰算法Java实现"},{"content":"\n代码逻辑 这道题的核心在于:\n不直接修改所有数据,而是记录一个全局更新时间戳。\n通过比较每个元素的更新时间和全局更新时间,动态判断应该返回哪个值。\n1. put(k, v) 1 2 3 4 5 6 7 8 9 10 11 public static void put(int k, int v) { if (map.containsKey(k)) { // 键已存在,更新值和时间戳 int[] value = map.get(k); value[0] = v; value[1] = cnt++; } else { // 新键,创建新的 [value, timestamp] 数组 map.put(k, new int[]{v, cnt++}); } } 每次存值时，不仅存具体的value，还会记录当前的cnt（时间戳），然后cnt自增。\n2. setAll(v) 1 2 3 4 public static void setAll(int v) { setAllValue = v; // 记录全局值 setAllTime = cnt++; // 记录全局更新时间 } 记录了 “全局值”setAllValue和 “全局更新时间”setAllTime（同样用cnt作为时间戳，然后cnt自增）。\n3. get(k) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static int get(int k) { if (!map.containsKey(k)) { return -1; } int[] value = map.get(k); // 比较时间戳 if (value[1] \u0026gt; setAllTime) { // 单个元素的更新时间晚于全局更新,返回元素自己的值 return value[0]; } else { // 单个元素的更新时间早于或等于全局更新,返回全局值 return setAllValue; } } 完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 public class SetAllHashMap { // 存储实际的键值对,值是一个数组 [value, timestamp] public static HashMap\u0026lt;Integer,int[]\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); // 全局设置的值 public static int setAllValue; // 全局设置的时间戳 public static int setAllTime; // 全局计数器,用于生成递增的时间戳 public static int cnt; public static void put(int k, int v) { if (map.containsKey(k)) { // 键已存在,更新值和时间戳 int[] value = map.get(k); value[0] = v; value[1] = cnt++; } else { // 新键,创建新的 [value, timestamp] 数组 map.put(k, new int[]{v, cnt++}); } } public static void setAll(int v) { setAllValue = v; // 记录全局值 setAllTime = cnt++; // 记录全局更新时间 } public static int get(int k) { if (!map.containsKey(k)) { return -1; } int[] value = map.get(k); // 比较时间戳 if (value[1] \u0026gt; setAllTime) { // 单个元素的更新时间晚于全局更新,返回元素自己的值 return value[0]; } else { // 单个元素的更新时间早于或等于全局更新,返回全局值 return setAllValue; } } public static int n,op,a,b; public static void main(String[] args)throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); while(in.nextToken()!=StreamTokenizer.TT_EOF){ map.clear(); setAllValue = 0; setAllTime = -1; cnt = 0; n = (int) in.nval; for(int i=0;i\u0026lt;n;i++){ in.nextToken(); op = (int) in.nval; if(op==1){ in.nextToken(); a=(int)in.nval; in.nextToken(); b = (int) in.nval; put(a,b); }else if(op==2){ in.nextToken(); a=(int)in.nval; out.println(get(a)); }else{ in.nextToken(); a=(int)in.nval; setAll(a); } } } out.flush(); out.close(); br.close(); } } ","date":"2025-10-20T21:55:45Z","permalink":"https://iamxurulin.github.io/p/o1%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6%E5%AE%9E%E7%8E%B0%E6%9C%89setall%E5%8A%9F%E8%83%BD%E7%9A%84%E5%93%88%E5%B8%8C%E8%A1%A8/","title":"O(1)时间复杂度实现有setAll功能的哈希表"},{"content":" 本文采用自底向上的归并排序来实现。\n代码逻辑 1. findEnd函数 用于找到从某个节点开始，往后数k个节点的位置。\n1 2 3 4 5 6 public static ListNode findEnd(ListNode s, int k) { while (s.next != null \u0026amp;\u0026amp; --k != 0) { s = s.next; } return s; } 2. merge函数 将两个有序部分合并成一个有序链表。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public static void merge(ListNode l1, ListNode r1, ListNode l2, ListNode r2) { ListNode pre; if (l1.val \u0026lt;= l2.val) { start = l1; pre = l1; l1 = l1.next; } else { start = l2; pre = l2; l2 = l2.next; } while (l1 != null \u0026amp;\u0026amp; l2 != null) { if (l1.val \u0026lt;= l2.val) { pre.next = l1; pre = l1; l1 = l1.next; } else { pre.next = l2; pre = l2; l2 = l2.next; } } if (l1 != null) { pre.next = l1; end = r1; } else { pre.next = l2; end = r2; } } 3. 处理流程 第一组的处理\n1 2 3 4 5 6 7 8 9 10 11 12 l1 = head; r1 = findEnd(l1, step); l2 = r1.next; r2 = findEnd(l2, step); next = r2.next; r1.next = null; r2.next = null; merge(l1, r1, l2, r2); head = start; lastTeamEnd = end; 后续组的处理\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 while (next != null) { l1 = next; r1 = findEnd(l1, step); l2 = r1.next; if (l2 == null) { lastTeamEnd.next = l1; break; } r2 = findEnd(l2, step); next = r2.next; r1.next = null; r2.next = null; merge(l1, r1, l2, r2); lastTeamEnd.next = start; lastTeamEnd = end; } 完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 public static ListNode sortList(ListNode head) { // 计算链表长度 int n = 0; ListNode cur = head; while (cur != null) { n++; cur = cur.next; } ListNode l1, r1, l2, r2, next, lastTeamEnd; // 步长从1开始，每次翻倍（1→2→4→...） for (int step = 1; step \u0026lt; n; step \u0026lt;\u0026lt;= 1) { l1 = head; r1 = findEnd(l1, step); l2 = r1.next; r2 = findEnd(l2, step); next = r2.next; // 切断链表，确保l1-r1和l2-r2是独立的子链表 r1.next = null; r2.next = null; // 合并两个子链表 merge(l1, r1, l2, r2); // 更新头节点（第一次合并后，头可能变） head = start; lastTeamEnd = end; // 处理剩余的子链表 while (next != null) { l1 = next; r1 = findEnd(l1, step); l2 = r1.next; // 如果只剩一个子链表，直接连接 if (l2 == null) { lastTeamEnd.next = l1; break; } r2 = findEnd(l2, step); next = r2.next; r1.next = null; r2.next = null; merge(l1, r1, l2, r2); // 连接上一组的尾和当前组的头 lastTeamEnd.next = start; lastTeamEnd = end; } } return head; } public static ListNode findEnd(ListNode s,int k){ while(s.next!=null\u0026amp;\u0026amp;--k!=0){ s=s.next; } return s; } public static ListNode start; // 合并后的头 public static ListNode end; // 合并后的尾 public static void merge(ListNode l1, ListNode r1, ListNode l2, ListNode r2) { ListNode pre; // 确定合并后的头节点 if (l1.val \u0026lt;= l2.val) { start = l1; pre = l1; l1 = l1.next; } else { start = l2; pre = l2; l2 = l2.next; } // 循环比较，合并节点 while (l1 != null \u0026amp;\u0026amp; l2 != null) { if (l1.val \u0026lt;= l2.val) { pre.next = l1; pre = l1; l1 = l1.next; } else { pre.next = l2; pre = l2; l2 = l2.next; } } // 处理剩余节点，确定合并后的尾 if (l1 != null) { pre.next = l1; end = r1; // l1有剩余，尾是r1 } else { pre.next = l2; end = r2; // l2有剩余，尾是r2 } } ","date":"2025-10-19T12:12:33Z","permalink":"https://iamxurulin.github.io/p/leetcode148-%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8/","title":"Leetcode148-排序链表"},{"content":" 这个问题在实际开发中也有应用场景,比如检测循环引用、内存泄漏分析等。\n本文介绍Floyd判圈算法来解决这道题。\n代码逻辑 设置两个指针,慢指针(slow)每次走一步,快指针(fast)每次走两步。\n如果链表存在环,快指针最终一定会追上慢指针；\n如果链表不存在环,快指针会先到达链表末尾。\n当快慢指针相遇后,问题就变成了:如何找到环的入口?\n这里用到了一个的数学性质:\n从起点到环入口的距离,等于从相遇点继续走到环入口的距离。\n因此,我们只需将一个指针移回起点，然后两个指针都以相同速度前进，当它们再次相遇的位置,就是环的入口。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public static ListNode detectCycle(ListNode head) { // 链表为空或节点数少于3个,不可能形成环 if (head == null || head.next == null || head.next.next == null) { return null; } // 初始化快慢指针 ListNode slow = head.next; ListNode fast = head.next.next; // 检测是否存在环 while (slow != fast) { // 快指针到达末尾,说明无环 if (fast.next == null || fast.next.next == null) { return null; } slow = slow.next; fast = fast.next.next; } // 找到环的入口 // 将fast指针移回起点 fast = head; // 两个指针以相同速度前进 while (slow != fast) { slow = slow.next; fast = fast.next; } // 相遇点即为环的入口 return slow; } ","date":"2025-10-18T10:33:19Z","permalink":"https://iamxurulin.github.io/p/floyd%E5%88%A4%E5%9C%88%E7%AE%97%E6%B3%95/","title":"Floyd判圈算法"},{"content":" 所谓回文，就是从前往后读和从后往前读都一样，比如 1→2→3→2→1 就是回文链表。\n代码逻辑 1.找到链表的中点 1 2 3 4 5 ListNode slow = head, fast = head; while (fast.next != null \u0026amp;\u0026amp; fast.next.next != null) { slow = slow.next; fast = fast.next.next; } 这里判断条件是 fast.next != null \u0026amp;\u0026amp; fast.next.next != null，能保证 slow 最终停在中间偏左的位置，无论链表长度是奇数还是偶数都适用。\n2.翻转后半部分链表 从中点开始，把后半部分链表反转。\n反转后，链表变成了一个\u0026quot;双向箭头\u0026quot;的结构：前半部分从 head 指向中点，后半部分从尾部指向中点。\n1 2 3 4 5 6 7 8 9 10 11 ListNode pre = slow; ListNode cur = pre.next; ListNode next = null; pre.next = null; // 断开前后两部分 while (cur != null) { next = cur.next; // 保存下一个节点 cur.next = pre; // 反转指针 pre = cur; // pre 前进 cur = next; // cur 前进 } 3.双指针比对值 现在有两个指针：left 从头开始往右走，right 从尾开始往左走。\n每一步比对两个节点的值，如果不相等就说明不是回文。\n1 2 3 4 5 6 7 8 9 10 11 12 boolean ans = true; ListNode left = head; ListNode right = pre; while (left != null \u0026amp;\u0026amp; right != null) { if (left.val != right.val) { ans = false; break; } left = left.next; right = right.next; } 4.恢复链表原状 判断完成后，不能把链表留成反转的状态，需要把后半部分再翻转回去。\n1 2 3 4 5 6 7 8 cur = pre.next; pre.next = null; while (cur != null) { next = cur.next; cur.next = pre; pre = cur; cur = next; } 完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public static boolean isPalindrome(ListNode head) { if (head == null || head.next == null) { return true; } ListNode slow = head, fast = head; // 找中点 while (fast.next != null \u0026amp;\u0026amp; fast.next.next != null) { slow = slow.next; fast = fast.next.next; } // 翻转后半部分 ListNode pre = slow; ListNode cur = pre.next; ListNode next = null; pre.next = null; while (cur != null) { next = cur.next; cur.next = pre; pre = cur; cur = next; } // 双指针比对 boolean ans = true; ListNode left = head; ListNode right = pre; while (left != null \u0026amp;\u0026amp; right != null) { if (left.val != right.val) { ans = false; break; } left = left.next; right = right.next; } // 恢复链表 cur = pre.next; pre.next = null; while (cur != null) { next = cur.next; cur.next = pre; pre = cur; cur = next; } return ans; } ","date":"2025-10-17T20:28:49Z","permalink":"https://iamxurulin.github.io/p/%E5%88%A4%E6%96%AD%E9%93%BE%E8%A1%A8%E6%98%AF%E5%90%A6%E4%B8%BA%E5%9B%9E%E6%96%87/","title":"判断链表是否为回文"},{"content":" 这道题的难点在于如何正确处理random指针的复制。\n常规思路是使用哈希表建立旧节点到新节点的映射,但这需要O(n)的额外空间。\n本文介绍一种空间复杂度O(1)的解法,核心是:\n在原链表的每个节点后面紧跟着插入一个克隆节点。\n1.创建克隆节点并插入原链表 遍历原链表,在每个原节点后面插入一个值相同的新节点,形成交替结构。\n1 2 3 4 5 6 7 8 9 Node cur = head; Node next = null; while (cur != null) { next = cur.next; // 保存下一个原节点 cur.next = new Node(cur.val); // 创建克隆节点 cur.next.next = next; // 克隆节点指向下一个原节点 cur = next; // 移动到下一个原节点 } 2.设置克隆节点的random指针 由于克隆节点紧跟在原节点后面，如果原节点A的random指向节点B，那么A的克隆节点A\u0026rsquo;的random应该指向B的克隆节点B\u0026rsquo;，而B\u0026rsquo;恰好就是B.next。\n1 2 3 4 5 6 7 8 9 10 11 cur = head; Node copy = null; while (cur != null) { next = cur.next.next; // 下一个原节点 copy = cur.next; // 当前的克隆节点 // 如果原节点的random不为空, // 则克隆节点的random指向原节点random的下一个节点(即克隆节点) copy.random = cur.random != null ? cur.random.next : null; cur = next; } 3.分离新旧链表 把交织在一起的新旧链表分离开,恢复原链表的结构,同时得到独立的新链表。\n1 2 3 4 5 6 7 8 9 10 11 12 13 Node ans = head.next; // 新链表的头节点 cur = head; while (cur != null) { next = cur.next.next; // 下一个原节点 copy = cur.next; // 当前克隆节点 cur.next = next; // 恢复原链表连接 // 连接克隆链表 copy.next = next != null ? next.next : null; cur = next; } return ans; 完整代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public static Node copyRandomList(Node head) { if (head == null) { return null; } Node cur = head; Node next = null; // 创建克隆节点并插入 while (cur != null) { next = cur.next; cur.next = new Node(cur.val); cur.next.next = next; cur = next; } cur = head; Node copy = null; // 设置random指针 while (cur != null) { next = cur.next.next; copy = cur.next; copy.random = cur.random != null ? cur.random.next : null; cur = next; } Node ans = head.next; cur = head; // 分离链表 while (cur != null) { next = cur.next.next; copy = cur.next; cur.next = next; copy.next = next != null ? next.next : null; cur = next; } return ans; } ","date":"2025-10-16T22:32:55Z","permalink":"https://iamxurulin.github.io/p/%E5%A4%8D%E5%88%B6%E5%B8%A6%E9%9A%8F%E6%9C%BA%E6%8C%87%E9%92%88%E7%9A%84%E9%93%BE%E8%A1%A8/","title":"复制带随机指针的链表"},{"content":" 这道题的难点不在于算法本身，而在于细节处理。\n代码逻辑 1.定位组的结束节点 从起始节点s开始，往后走k-1步，找到当前组的最后一个节点。如果中途遇到null，说明不足k个节点。\n1 2 3 4 5 6 public static ListNode teamEnd(ListNode s, int k) { while (--k != 0 \u0026amp;\u0026amp; s != null) { s = s.next; } return s; } 2.翻转一组节点 提前保存e.next，这样翻转完成后，原来的起始节点s可以直接连接到下一组。\n1 2 3 4 5 6 7 8 9 10 11 public static void reverse(ListNode s, ListNode e) { e = e.next; // 先保存下一组的起始节点 ListNode pre = null, cur = s, next = null; while (cur != e) { next = cur.next; cur.next = pre; pre = cur; cur = next; } s.next = e; // 翻转后，原来的起始节点要连接到下一组 } 3.主函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static ListNode reverseKGroup(ListNode head, int k) { ListNode start = head; ListNode end = teamEnd(start, k); if (end == null) { return head; // 第一组不足k个，直接返回 } // 第一组特殊处理：需要更新头节点 head = end; reverse(start, end); ListNode lastTeamEnd = start; // 翻转后start变成了上一组的尾节点 // 处理后续各组 while (lastTeamEnd.next != null) { start = lastTeamEnd.next; end = teamEnd(start, k); if (end == null) { return head; // 当前组不足k个，保持原样 } reverse(start, end); lastTeamEnd.next = end; // 连接上一组和当前组 lastTeamEnd = start; // 更新上一组的尾节点 } return head; } 完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public static ListNode reverseKGroup(ListNode head, int k) { ListNode start = head; ListNode end = teamEnd(start, k); if (end == null) { return head; // 第一组不足k个，直接返回 } // 第一组特殊处理：需要更新头节点 head = end; reverse(start, end); ListNode lastTeamEnd = start; // 翻转后start变成了上一组的尾节点 // 处理后续各组 while (lastTeamEnd.next != null) { start = lastTeamEnd.next; end = teamEnd(start, k); if (end == null) { return head; // 当前组不足k个，保持原样 } reverse(start, end); lastTeamEnd.next = end; // 连接上一组和当前组 lastTeamEnd = start; // 更新上一组的尾节点 } return head; } //定位组的结束节点 public static ListNode teamEnd(ListNode s, int k){ while(--k!=0\u0026amp;\u0026amp;s!=null){ s=s.next; } return s; } public static void reverse(ListNode s, ListNode e) { e = e.next; // 先保存下一组的起始节点 ListNode pre = null, cur = s, next = null; while (cur != e) { next = cur.next; cur.next = pre; pre = cur; cur = next; } s.next = e; // 翻转后，原来的起始节点要连接到下一组 } ","date":"2025-10-15T20:51:43Z","permalink":"https://iamxurulin.github.io/p/k%E4%B8%AA%E4%B8%80%E7%BB%84%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A8-%E4%B8%80%E9%81%93%E8%AE%A9%E4%BA%BA%E5%A4%B4%E7%96%BC%E7%9A%84%E7%AE%97%E6%B3%95%E9%A2%98/","title":"K个一组翻转链表-一道让人头疼的算法题"},{"content":" 需要注意的是，这里的\u0026quot;相交\u0026quot;是指两个链表在某个节点处合并，之后共享剩余的节点。\n这道题的关键在于:如果两个链表相交,它们一定有共同的尾节点。\n1.首先遍历两个链表，计算两个链表的长度差，然后找到各自的尾节点。 1 2 3 4 5 6 7 8 9 10 11 12 13 ListNode a = h1, b = h2; int diff = 0; while (a.next != null) { a = a.next; diff++; } while (b.next != null) { b = b.next; diff--; } if (a != b) { return null; } 用一个变量diff同时记录长度差。\n遍历h1时diff++,遍历h2时diff\u0026ndash;,最终diff就是两个链表的长度差。\n遍历结束后,如果a != b,说明两个链表的尾节点不同,必然不相交。\n2.让较长的链表先走若干步,消除长度差。 1 2 3 4 5 6 7 8 9 10 11 if (diff \u0026gt;= 0) { a = h1; // h1更长 b = h2; } else { a = h2; // h2更长 b = h1; } diff = Math.abs(diff); while (diff-- != 0) { a = a.next; // 长链表先走 } 3.两个指针距离各自链表尾部的距离相同,接下来同步前进,第一个相同的节点就是交点。 1 2 3 4 5 while (a != b) { a = a.next; b = b.next; } return a; 完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public static ListNode getIntersectionNode(ListNode h1,ListNode h2){ if(h1==null||h2==null){ return null; } ListNode a = h1,b=h2; int diff =0 ; //计算长度差，找到h1的尾节点 while(a.next!=null){ a=a.next; diff++; } //计算长度差，找到h2的尾节点 while(b.next!=null){ b= b.next; diff--; } //尾节点不同，不相交 if(a!=b){ return null; } //让a指向较长的链表，b指向较短的链表 if(diff\u0026gt;=0){ a=h1; b=h2; }else{ a=h2; b=h1; } //长链表先走diff步 diff=Math.abs(diff); while(diff--!=0){ a=a.next; } //同步前进，找到交点 while(a!=b){ a=a.next; b=b.next; } return a; } ","date":"2025-10-14T20:33:43Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%80%E6%AC%A1%E9%81%8D%E5%8E%86%E6%89%BE%E5%88%B0%E9%93%BE%E8%A1%A8%E4%BA%A4%E7%82%B9%E7%9A%84%E4%BC%98%E9%9B%85%E8%A7%A3%E6%B3%95/","title":"一次遍历找到链表交点的优雅解法"},{"content":" 本文借这道题，分析如何用位运算实现一套完整的整数运算体系。\n1. 加法运算 1 2 3 4 5 6 7 8 9 public static int add(int a, int b) { int ans = a; while (b != 0) { ans = a ^ b; // 无进位相加 b = (a \u0026amp; b) \u0026lt;\u0026lt; 1; // 计算进位 a = ans; } return ans; } 2. 取反运算 1 2 3 public static int neg(int n) { return add(~n, 1); } 经典的补码取反公式:一个数的相反数等于按位取反加1。\n3. 减法运算 1 2 3 public static int minus(int a, int b) { return add(a, neg(b)); } 4.乘法运算 1 2 3 4 5 6 7 8 9 10 11 public static int multiply(int a, int b) { int ans = 0; while (b != 0) { if ((b \u0026amp; 1) != 0) { ans = add(ans, a); } a \u0026lt;\u0026lt;= 1; b \u0026gt;\u0026gt;\u0026gt;= 1; } return ans; } 思路\n将乘法转化为加法和移位操作,本质是将b的二进制展开:\n1 2 a × b = a × (b₀×2⁰ + b₁×2¹ + b₂×2² + ...) = a×b₀ + (a\u0026lt;\u0026lt;1)×b₁ + (a\u0026lt;\u0026lt;2)×b₂ + ... 每次检查b的最低位,如果为1则累加当前的a,然后a左移(相当于×2),b右移(处理下一位)。\n5.除法运算 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static int divide(int a, int b) { if (a == MIN \u0026amp;\u0026amp; b == MIN) { return 1; // 最小值除以最小值 = 1 } if (a != MIN \u0026amp;\u0026amp; b != MIN) { return div(a, b); // 都不是最小值,正常处理 } if (b == MIN) { return 0; // 任何非最小值除以最小值 = 0 } if (b == neg(1)) { return Integer.MAX_VALUE; // 题目要求溢出返回最大值 } // a是最小值,b不是最小值也不是-1 a = add(a, b \u0026gt; 0 ? b : neg(b)); int ans = div(a, b); int offset = b \u0026gt; 0 ? neg(1) : 1; return add(ans, offset); } 思路\n当a是最小值时:\n先将a向0的方向移动|b|个单位:a = a + |b| 对调整后的a进行除法:ans = div(a, b) 补偿之前的调整: 如果b \u0026gt; 0:商应该减1(因为我们让被除数变大了) 如果b \u0026lt; 0:商应该加1(因为负数除法的特性) 函数:div 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static int div(int a, int b) { int x = a \u0026lt; 0 ? neg(a) : a; // 转为正数 int y = b \u0026lt; 0 ? neg(b) : b; int ans = 0; for (int i = 30; i \u0026gt;= 0; i = minus(i, 1)) { if ((x \u0026gt;\u0026gt; i) \u0026gt;= y) { ans |= (1 \u0026lt;\u0026lt; i); x = minus(x, y \u0026lt;\u0026lt; i); } } return a \u0026lt; 0 ^ b \u0026lt; 0 ? neg(ans) : ans; } 思路\n类似于二分查找,从高位到低位试探商的每一位:\n将x和y都转为正数 从第30位开始(第31位是符号位)向下遍历 检查x \u0026gt;\u0026gt; i是否大于等于y: 如果是,说明商的第i位是1,同时从x中减去y \u0026lt;\u0026lt; i 最后根据符号决定结果的正负 ","date":"2025-10-13T20:40:59Z","permalink":"https://iamxurulin.github.io/p/%E4%BD%8D%E8%BF%90%E7%AE%97%E5%AE%9E%E7%8E%B0%E6%95%B4%E6%95%B0%E5%8A%A0%E5%87%8F%E4%B9%98%E9%99%A4%E8%BF%90%E7%AE%97/","title":"位运算实现整数加减乘除运算"},{"content":" 在处理大规模布尔数组时,我们经常面临空间和时间效率的双重挑战。\n如果用普通的 boolean 数组,每个元素至少占用 1 字节,当数据量达到百万级别时,内存开销会非常可观。\n而位集合(Bitset)通过将每个布尔值压缩为一个比特位,可以将空间占用降低到原来的 1/32。\n以下是一个支持延迟翻转优化的高效 Bitset 实现。\n代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 class Bitset{ private int[] set;//整形数组，用于存储位信息 private final int size;//位集合的逻辑大小（用户视角的位数） private int zeros;//统计当前值为0的位的数量 private int ones;//统计当前值为1的位的数量 private boolean reverse;//反转标志 //构造函数 public Bitset(int n){ set = new int[(n+31)/32]; size = n; zeros = n; ones = 0; reverse = false; } public void fix(int i){ int index = i/32; int bit = i%32; if(!reverse){ //正常模式reverse=false，将0改为1 if((set[index]\u0026amp;(1\u0026lt;\u0026lt;bit))==0){ zeros--; ones++; set[index]|=(1\u0026lt;\u0026lt;bit); } }else{ //反转模式reverse=true，将1改为0 if((set[index]\u0026amp;(1\u0026lt;\u0026lt;bit))!=0){ zeros--; ones++; set[index]^=(1\u0026lt;\u0026lt;bit); } } } public void unfix(int i){ int index = i/32; int bit = i%32; if(!reverse){ //正常模式reverse=false，将1改为0 if((set[index]\u0026amp;(1\u0026lt;\u0026lt;bit))!=0){ ones--; zeros++; set[index]^=(1\u0026lt;\u0026lt;bit); } }else{ //反转模式reverse=true，将0改为1 if((set[index]\u0026amp;(1\u0026lt;\u0026lt;bit))==0){ ones--; zeros++; set[index]|=(1\u0026lt;\u0026lt;bit); } } } //翻转所有位（0变1，1变0） public void flip(){ reverse = !reverse; int tmp = zeros; zeros=ones; ones = tmp; } //检查是否全为1 public boolean all(){ return ones == size; } //检查是否至少有一个1 public boolean one(){ return ones\u0026gt;0; } //返回1的数量 public int count(){ return ones; } //将位集合转换为字符串表示 public String toString(){ StringBuilder builder = new StringBuilder(); for(int i=0,k=0,number,status;i\u0026lt;size;k++){ number = set[k]; for(int j=0;j\u0026lt;32\u0026amp;\u0026amp;i\u0026lt;size;j++,i++){ status = (number\u0026gt;\u0026gt;j)\u0026amp;1; status ^= reverse?1:0; builder.append(status); } } return builder.toString(); } } ","date":"2025-10-12T22:26:58Z","permalink":"https://iamxurulin.github.io/p/leetcode2166-%E8%AE%BE%E8%AE%A1%E4%BD%8D%E9%9B%86/","title":"Leetcode2166-设计位集"},{"content":" 这道题如果用哈希表统计次数，空间复杂度是 O (n)；\n如果用异或，只能处理 “出现偶数次抵消” 的情况（比如出现 2 次），面对 3 次、5 次这类奇数次数就失效了。\n首先，这里用到了一个核心规律：\n对于任意一个二进制位（比如第 0 位、第 1 位），如果某个数出现了 m 次，那么这个数在该位上的 1 的总贡献，一定是 m 的倍数。\n代码逻辑 1. 按位统计 1 的个数 1 2 3 4 5 6 int[] cnts = new int[32]; for (int num : arr) { for (int i = 0; i \u0026lt; 32; i++) { cnts[i] += (num \u0026gt;\u0026gt; i) \u0026amp; 1; } } 2. 重构目标数 1 2 3 4 5 6 int ans = 0; for (int i = 0; i \u0026lt; 32; i++) { if (cnts[i] % m != 0) { ans |= (1 \u0026lt;\u0026lt; i); } } 完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // LeetCode 137（其他数出现3次，目标数出现1次） public static int singleNumber(int[] nums) { return find(nums, 3); // 直接调用通用解法，m=3 } // 通用解法：数组中只有1种数出现次数\u0026lt;m，其他数都出现m次，返回该数 public static int find(int[] arr, int m) { // cnts[i]：统计数组中所有数在第i位（0~31）上1的总个数 int[] cnts = new int[32]; // 遍历数组，统计每一位上1的总个数 for (int num : arr) { for (int i = 0; i \u0026lt; 32; i++) { // 提取num第i位的1，累加到cnts[i] cnts[i] += (num \u0026gt;\u0026gt; i) \u0026amp; 1; } } // 根据cnts重构目标数 int ans = 0; for (int i = 0; i \u0026lt; 32; i++) { // 如果第i位1的总个数不是m的倍数，说明目标数第i位是1 if (cnts[i] % m != 0) { // 把第i位设为1 ans |= (1 \u0026lt;\u0026lt; i); } } return ans; } ","date":"2025-10-11T22:38:35Z","permalink":"https://iamxurulin.github.io/p/%E7%94%A8-%E6%8C%89%E4%BD%8D%E7%BB%9F%E8%AE%A1-%E6%89%BE%E5%94%AF%E4%B8%80%E5%87%BA%E7%8E%B0%E5%B0%91%E4%BA%8E-3-%E6%AC%A1%E7%9A%84%E6%95%B0/","title":"用-按位统计-找唯一出现少于-3-次的数"},{"content":" 如果是只有一个元素只出现一次，那么可以用异或抵消出现两次的元素，从而找出那个只出现一次的元素。\n现在是恰好有两个元素只出现一次，那这道题的难点就在于：\n如何把 “这两个只出现一次的元素” 拆分开，再分别用异或抵消出现两次的元素的方式，从而找出这两个只出现一次的元素。\n1.计算所有数的异或结果 根据异或运算的性质，出现偶数次的数会互相抵消，最后剩下的就是两个奇数次的数的异或结果。\n1 2 3 4 int eor1 = 0; for (int num : nums) { eor1 ^= num; } 2.用 Brian Kernighan 算法提取最右侧的 1 1 int rightOne = eor1 \u0026amp; (-eor1); 3.分组异或得到其中一个目标数 因为rightOne是a ^ b最右侧的 1，这意味着 a 和 b 在rightOne对应的位上，一个是 0，一个是 1，可以此作为依据进行分组。\n1 2 3 4 5 6 int eor2 = 0; for (int num : nums) { if ((num \u0026amp; rightOne) == 0) { eor2 ^= num; } } 4.得到另一个目标数 1 return new int[] { eor2, eor1 ^ eor2 }; 完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static int[] singleNumber(int[] nums) { // 计算所有数的异或结果，得到 a ^ b int eor1 = 0; for (int num : nums) { eor1 ^= num; // 偶数次的数抵消，最后剩下 a^b } // 提取 eor1 二进制中最右侧的1 int rightOne = eor1 \u0026amp; (-eor1); // 按 rightOne 分组，异或得到其中一个目标数 a int eor2 = 0; for (int num : nums) { // 判断 num 在 rightOne 对应的位上是否为0 if ((num \u0026amp; rightOne) == 0) { eor2 ^= num; // 该组中偶数次的数抵消，剩下 a } } // 得到另一个目标数 b = a ^ (a^b) = eor2 ^ eor1 return new int[] { eor2, eor1 ^ eor2 }; } ","date":"2025-10-11T08:39:06Z","permalink":"https://iamxurulin.github.io/p/%E7%94%A8%E5%BC%82%E6%88%96%E6%89%BE-%E4%B8%A4%E4%B8%AA%E5%8F%AA%E5%87%BA%E7%8E%B0%E4%B8%80%E6%AC%A1%E7%9A%84%E6%95%B0/","title":"用异或找-两个只出现一次的数"},{"content":" 这道题大多数人的第一反应是 “求和法”（用 0~n 的和减去数组总和），或者 “哈希表法”（存出现过的数再遍历查找）。\n但今天本文要讲的异或解法，不仅时间复杂度和求和法一样是 $O (n)$，空间复杂度更是最优的 $O (1)$。\n数组 nums 包含 [0, n] 中的 n 个数，理论上应该有n+1个数，实际上只有n个数，说明正好缺了一个。\n也就是说：\n完整的数字集合是：0, 1, 2, \u0026hellip;, n（共 n+1 个数）；\n数组中的数字集合是：完整集合去掉一个数（共 n 个数）。\n想想看，如果我们把 “完整集合的所有数” 和 “数组中的所有数” 都做一次异或，会发生什么？\n根据异或的性质：\n完整集合和数组中都出现的数会互相抵消，最后剩下的，就是 “完整集合有、数组没有” 的那个数 —— 也就是我们要找的缺失数字。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static int missingNumber(int[] nums) { // eorAll：存储 0~n 所有数的异或结果 // eorHas：存储数组 nums 所有数的异或结果 int eorAll = 0, eorHas = 0; for (int i = 0; i \u0026lt; nums.length; i++) { eorAll ^= i; // eorAll 异或 0~nums.length-1 eorHas ^= nums[i]; // eorHas 异或数组中的每个数 } // 补全 eorAll，异或上 nums.length（因为完整集合是 0~n，循环漏了 n） eorAll ^= nums.length; // eorAll ^ eorHas = 缺失数字 return eorAll ^ eorHas; } ","date":"2025-10-10T07:35:15Z","permalink":"https://iamxurulin.github.io/p/%E5%AF%BB%E6%89%BE%E4%B8%A2%E5%A4%B1%E7%9A%84%E6%95%B0%E5%AD%97/","title":"寻找丢失的数字"},{"content":" 这道题如果用 a-b 的符号判断，在两数处于比较极端的情况下可能会发生整数溢出。\n比如当 a 是Integer.MIN_VALUE（-2147483648）、b 是Integer.MAX_VALUE（2147483647）时，a-b 会溢出成正数，导致错误地认为 a 比 b 大。\n本文接下来介绍一种新颖的解法。\n代码逻辑 1.flip：翻转0和1 1 2 3 4 // 必须保证n是0或1，返回翻转后的值 public static int flip(int n) { return n ^ 1; // 异或1：0^1=1，1^1=0 } 2.sign：判断数字符号 int 是 32 位，最高位（第 31 位）是 “符号位”——0 表示非负（正数或 0），1 表示负数。 经过flip 翻转后非负数从 0→1，负数从 1→0。\n1 2 3 4 // 非负数返回1，负数返回0 public static int sign(int n) { return flip(n \u0026gt;\u0026gt;\u0026gt; 31); // 无符号右移31位后翻转 } 3.核心方法getMax 当 a 和 b 符号不同时，直接根据符号判断； 当符号相同时，再用 a-b 的符号判断。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static int getMax(int a, int b) { int c = a - b; // 计算a、b、c的符号 int sa = sign(a); // a非负→1，负→0 int sb = sign(b); // b非负→1，负→0 int sc = sign(c); // c非负→1，负→0（c可能溢出） // 判断a和b的符号是否不同：diffAB=1（不同），diffAB=0（相同） int diffAB = sa ^ sb; // 不同为1，相同为0 // 判断a和b的符号是否相同：相同为1，不同为0 int sameAB = flip(diffAB); // 确定returnA（选a的权重：1选a，0选b） // 符号不同时（diffAB=1）：选非负的那个（sa=1→a非负，选a） // 符号相同时（sameAB=1）：选c非负的那个（sc=1→a\u0026gt;=b，选a） int returnA = diffAB * sa + sameAB * sc; int returnB = flip(returnA); // 选b的权重 // 返回最大值 return a * returnA + b * returnB; } 完整代码\n链接：https://pan.quark.cn/s/7efcb9d22a2f 提取码：jdgT\n","date":"2025-10-09T20:18:41Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%8D%E4%BD%BF%E7%94%A8%E6%AF%94%E8%BE%83%E8%BF%90%E7%AE%97%E7%AC%A6-%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96%E4%B8%A4%E4%B8%AA%E6%95%B4%E6%95%B0%E7%9A%84%E6%9C%80%E5%A4%A7%E5%80%BC/","title":"不使用比较运算符-如何获取两个整数的最大值"},{"content":"在排序算法里，我们总习惯说 “快排是平均效率最高的”，但在某些场景下 —— 比如给手机号排序、给 10 万条 “0-9999” 的数字排序 ——基数排序的速度会远超快排。\n原因很简单：基数排序是 “非比较排序”，不需要通过元素间的比较来确定顺序，而是靠 “按位分组” 实现排序，时间复杂度能稳定在 O (n*k)（k 是数字的位数）。\n基数排序的核心其实就是把数字按每一位（个位、十位、百位\u0026hellip;）拆开来排序。\n代码逻辑 1.找最小值（处理负数） 考虑到如果数组有负数，按位提取会出问题，所以需要先把所有数转成非负。\n1 2 3 4 int min = arr[0]; for (int i = 1; i \u0026lt; n; i++) { min = Math.min(min, arr[i]); } 2.平移数组 + 找最大值 平移后数组使得所有元素均≥0；\n记录最大值max，是为了通过bits(max)计算 “需要排序几轮”。\n1 2 3 4 5 int max = 0; for (int i = 0; i \u0026lt; n; i++) { arr[i] -= min; max = Math.max(max, arr[i]); } 3.bits（计算位数） 1 2 3 4 5 6 7 8 public static int bits(int number) { int ans = 0; while (number \u0026gt; 0) { ans++; number /= BASE; } return ans; } 4.radixSort（按位排序） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public static void radixSort(int[] arr, int n, int bits) { // offset：当前处理的位数（比如：1=个位，10=十位，100=百位...） for (int offset = 1; bits \u0026gt; 0; offset *= BASE, bits--) { // 初始化计数数组（每次处理新的位，都要清空） Arrays.fill(cnts, 0); // 统计当前位（offset对应的位）上，0~BASE-1的出现次数 for (int i = 0; i \u0026lt; n; i++) { // 提取arr[i]在当前位的数字 int digit = (arr[i] / offset) % BASE; cnts[digit]++; } // 将计数数组转为“前缀和数组”（确定元素在help中的位置） for (int i = 1; i \u0026lt; BASE; i++) { cnts[i] += cnts[i - 1]; } // 逆序遍历原数组，填充辅助数组help for (int i = n - 1; i \u0026gt;= 0; i--) { // 再次提取当前位数字 int digit = (arr[i] / offset) % BASE; // 前缀和-1：得到当前元素在help中的索引 help[--cnts[digit]] = arr[i]; } // 将辅助数组的结果复制回原数组（完成当前位的排序） for (int i = 0; i \u0026lt; n; i++) { arr[i] = help[i]; } } } 完整代码 链接：https://pan.quark.cn/s/3071ab17556e\n","date":"2025-10-08T16:52:18Z","permalink":"https://iamxurulin.github.io/p/%E5%9F%BA%E6%95%B0%E6%8E%92%E5%BA%8F%E5%88%86%E6%9E%90/","title":"基数排序分析"},{"content":" 解决这个问题的关键是：每次必须选择当前最大的元素减半。\n如果要高效地 “每次获取最大元素”，大根堆无疑是最佳选择。\n方法 1：用 PriorityQueue 实现 使用 Java 内置的PriorityQueue作为大根堆，直接存储double类型的元素。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public static int halveArray(int[] nums) { // 大根堆：通过比较器(b.compareTo(a))实现，确保堆顶是最大元素 PriorityQueue\u0026lt;Double\u0026gt; heap = new PriorityQueue\u0026lt;\u0026gt;((a, b) -\u0026gt; b.compareTo(a)); double sum = 0; // 计算数组总和，并将所有元素加入大根堆 for (int num : nums) { heap.add((double) num); sum += num; } // 减少的总量至少为原总和的一半 sum /= 2; int ans = 0; double minus = 0; // 累计减少的总量 // 每次取最大元素减半，直到累计减少量≥目标 while (minus \u0026lt; sum) { ans++; // 操作次数+1 double cur = heap.poll() / 2; // 取出最大元素，减半 minus += cur; heap.add(cur); // 减半后的元素放回堆中 } return ans; } 方法 2：自定义大根堆（用整数避免浮点数精度问题） 使用自定义的大根堆，并且用long类型存储元素（通过左移 20 位放大元素），避开浮点数精度陷阱。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public int halveArray(int[] nums) { int n = nums.length; long[] heap = new long[n]; long sum = 0; for (int i = n - 1; i \u0026gt;= 0; i--) { // 放大2^20倍（足够覆盖1e9的精度，避免浮点数） heap[i] = (long) nums[i] \u0026lt;\u0026lt; 20; sum += heap[i]; heapify(heap, n, i); } sum /= 2; int ans = 0; long minus = 0; while (minus \u0026lt; sum) { ans++; // 堆顶减半（整数操作，无精度问题） heap[0] \u0026gt;\u0026gt;= 1; // 右移1位 = 除以2，比/=2更快 minus += heap[0]; // 调整堆顶 heapify(heap, n, 0); } return ans; } private void heapify(long[] heap, int size, int i) { // 暂存当前节点值，避免重复访问heap[i] long curVal = heap[i]; // 左孩子索引 int left = i * 2 + 1; while (left \u0026lt; size) { // 暂存左孩子值，减少数组访问 long leftVal = heap[left]; // 右孩子值：存在则取，不存在则设为极小值（不影响比较） long rightVal = (left + 1 \u0026lt; size) ? heap[left + 1] : Long.MIN_VALUE; //选左右孩子中更大的那个 int largerChild = (rightVal \u0026gt; leftVal) ? left + 1 : left; long largerVal = (largerChild == left) ? leftVal : rightVal; //若当前节点值 \u0026gt;= 较大孩子值，堆结构已满足，退出 if (curVal \u0026gt;= largerVal) { break; } //直接将较大孩子值覆盖到当前节点 heap[i] = largerVal; // 更新当前节点索引，继续向下调整 i = largerChild; left = i * 2 + 1; } //将暂存的当前节点值放到最终位置 heap[i] = curVal; } 完整代码 链接：https://pan.quark.cn/s/2ed207e367ac 提取码：3H47\n","date":"2025-10-07T23:39:44Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%A4%E7%A7%8D%E6%96%B9%E6%B3%95%E8%A7%A3%E5%86%B3%E5%B0%86%E6%95%B0%E7%BB%84%E5%92%8C%E5%87%8F%E5%8D%8A%E7%9A%84%E6%9C%80%E5%B0%91%E6%93%8D%E4%BD%9C%E6%AC%A1%E6%95%B0/","title":"两种方法解决「将数组和减半的最少操作次数」"},{"content":"在处理区间问题时，我们经常会遇到需要找出“最多有多少个事件同时发生”的场景。\n比如会议室的最大占用数、资源的最大并发访问量等等。\n本文分析一道经典的线段重合问题。\n代码逻辑 输入输出 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static void main(String[] args) throws IOException { // 输入 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); // 输出 PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 多组输入：直到读取到文件末尾 while (in.nextToken() != StreamTokenizer.TT_EOF) { // 读取线段总数n n = (int) in.nval; // 读取n条线段的开始和结束位置 for (int i = 0; i \u0026lt; n; i++) { in.nextToken(); // 读取开始位置 line[i][0] = (int) in.nval; in.nextToken(); // 读取结束位置 line[i][1] = (int) in.nval; } // 计算最大重合数并输出 out.println(compute()); } // 关闭流 out.flush(); out.close(); br.close(); } compute 方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static int compute() { size = 0; // 每次计算前重置堆大小 // 按线段的「开始位置」升序排序 Arrays.sort(line, 0, n, (a, b) -\u0026gt; a[0] - b[0]); int ans = 0; // 记录最大重合数 // 遍历每条线段，维护堆并计算重合数 for (int i = 0; i \u0026lt; n; i++) { int curStart = line[i][0]; // 当前线段的开始位置 int curEnd = line[i][1]; // 当前线段的结束位置 // 移除堆中「已结束」的线段（堆顶是最早结束的，先判断它） // 堆不为空，且堆顶的结束位置 ≤ 当前线段开始位置 → 不重合，弹出 while (size \u0026gt; 0 \u0026amp;\u0026amp; heap[0] \u0026lt;= curStart) { pop(); } // 把当前线段的结束位置加入堆（当前线段开始参与重合） add(curEnd); // 更新最大重合数（堆的大小就是当前重合数） ans = Math.max(ans, size); } return ans; } 小根堆的实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 // 新元素向上调整 public static void add(int x) { // 新元素先放到堆的最后一位 heap[size] = x; int i = size++; // i是新元素的初始索引 // 向上调整：和父节点比较，比父节点小就交换 // 父节点索引 = (i-1)/2（整数除法，自动向下取整） while (heap[i] \u0026lt; heap[(i - 1) / 2]) { swap(i, (i - 1) / 2); i = (i - 1) / 2; 比较 } } // 堆顶（最小值）移除，向下调整 public static void pop() { // 堆顶（索引0）和堆的最后一位元素交换 swap(0, --size); // 交换后，size-1 → 堆顶元素被“移除” int i = 0; // 从堆顶开始向下调整 int l = 1; // 左孩子索引 = 2*i + 1 // 向下调整 while (l \u0026lt; size) { // 选左右孩子中「更小」的那个（右孩子存在且更小才选右） int best = (l + 1 \u0026lt; size \u0026amp;\u0026amp; heap[l + 1] \u0026lt; heap[l]) ? l + 1 : l; // 比较“最小孩子”和当前节点 best = (heap[best] \u0026lt; heap[i]) ? best : i; if (best == i) { break; } // 交换当前节点和最优孩子，继续向下调整 swap(i, best); i = best; l = i * 2 + 1; } } 完整代码\n链接：https://pan.quark.cn/s/7ec0540d754b 提取码：iMn8\n","date":"2025-10-07T12:26:33Z","permalink":"https://iamxurulin.github.io/p/acm%E6%A8%A1%E5%BC%8F%E7%BB%8F%E5%85%B8%E9%97%AE%E9%A2%98-%E7%BA%BF%E6%AE%B5%E9%87%8D%E5%90%88/","title":"ACM模式经典问题-线段重合"},{"content":" 这道题的思路是用小根堆 “盯紧” 最小节点。\n核心步骤如下：\n先把 K 个链表的头节点放进小根堆，这样堆顶就是当前最小节点； 每次从堆顶取出最小节点，加入结果链表； 再把这个最小节点的下一个节点放进堆里（如果有的话）； 重复步骤 2-3，直到堆为空，所有节点都被合并。 1.初始化小根堆 Java 的PriorityQueue默认是小根堆，但这里显式传入比较器(a, b) -\u0026gt; a.val - b.val，确保按节点值升序排序。\n1 PriorityQueue\u0026lt;ListNode\u0026gt; heap = new PriorityQueue\u0026lt;\u0026gt;((a, b) -\u0026gt; a.val - b.val); 2.加入所有链表的头节点 遍历输入的 K 个链表，把非空的头节点加入堆。\n1 2 3 4 5 for (ListNode h : arr) { if (h != null) { heap.add(h); } } 3. 构建结果链表的头部 先从堆顶取出最小的节点（h），作为结果链表的头节点。\n用pre指针跟踪结果链表的尾部。\n把h的下一个节点加入堆（如果存在）。\n1 2 3 4 5 ListNode h = heap.poll(); ListNode pre = h; if (pre.next != null) { heap.add(pre.next); } 4.循环合并剩余节点 每次从堆顶取最小节点cur，拼到pre后面，然后pre移到cur。\n再把cur的下一个节点加入堆。\n1 2 3 4 5 6 7 8 while (!heap.isEmpty()) { ListNode cur = heap.poll(); pre.next = cur; pre = cur; if (cur.next != null) { heap.add(cur.next); } } 完整代码\n链接：https://pan.quark.cn/s/8a7bb143e196 提取码：dXXp\n","date":"2025-10-06T19:08:27Z","permalink":"https://iamxurulin.github.io/p/%E7%94%A8%E5%A0%86%E7%A7%92%E6%9D%80%E5%90%88%E5%B9%B6-k-%E4%B8%AA%E5%B7%B2%E6%8E%92%E5%BA%8F%E7%9A%84%E9%93%BE%E8%A1%A8/","title":"用堆秒杀「合并-K-个已排序的链表」"},{"content":"本文用大根堆来实现升序排序。\n大根堆其实是一种特殊的“完全二叉树”，要求 “父节点的值大于子节点的值”。\n在使用数组存储时有以下索引关系：\n若父节点索引为i，则左孩子索引是2i+1，右孩子索引是2i+2； 若子节点索引为i，则父节点索引是(i-1)/2（整数除法，向下取整）。 heapInsert：向上调整 不断和父节点比较，若比父节点大就交换，直到父节点更大或到堆顶（索引 0）。\n1 2 3 4 5 6 7 public static void heapInsert(int[] arr, int i) { // 父节点索引=(i-1)/2，只要当前元素\u0026gt;父节点，就交换 while (arr[i] \u0026gt; arr[(i - 1) / 2]) { swap(arr, i, (i - 1) / 2); i = (i - 1) / 2; // 交换后，当前元素跑到父节点位置，继续向上比 } } heapify：向下调整 先找左右孩子里的 “最大值”，再和当前元素比较，若孩子更大就交换，直到当前元素更大或没有孩子。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static void heapify(int[] arr, int i, int size) { int l = i * 2 + 1; // 左孩子索引 while (l \u0026lt; size) { // 只要有左孩子，就继续循环 // 1. 选左右孩子中的最大值（若有右孩子，且右孩子\u0026gt;左孩子，则选右） int best = (l + 1 \u0026lt; size \u0026amp;\u0026amp; arr[l + 1] \u0026gt; arr[l]) ? l + 1 : l; // 2. 比较“最大值”和当前元素，选更大的那个 best = arr[best] \u0026gt; arr[i] ? best : i; // 3. 若当前元素已是最大，说明堆结构ok，退出 if (best == i) break; // 4. 否则交换，继续向下沉 swap(arr, best, i); i = best; l = i * 2 + 1; } } 方式一：heapSort（从顶到底建堆） 把数组当 “空堆”，逐个元素插入堆尾，用 heapInsert 向上调整\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static void heapSort(int[] arr) { int n = arr.length; // 从顶到底建堆 for (int i = 0; i \u0026lt; n; i++) { heapInsert(arr, i); } // 排序（反复交换堆顶和堆尾，向下调整） int size = n; while (size \u0026gt; 1) { // 交换堆顶（最大值）和堆尾元素，最大值放到最终位置 swap(arr, 0, --size); // 堆顶元素变了，向下调整维持大根堆 heapify(arr, 0, size); } } 方式二：heapSort（从底到顶建堆） 从 “最后一个非叶子节点” 开始，用 heapify 向下调整，直到堆顶。\n1 2 3 4 5 6 7 8 9 10 11 12 13 public static void heapSort2(int[] arr) { int n = arr.length; // 从底到顶建堆（从最后一个节点开始，向下调整） for (int i = n - 1; i \u0026gt;= 0; i--) { heapify(arr, i, n); } // 排序 int size = n; while (size \u0026gt; 1) { swap(arr, 0, --size); heapify(arr, 0, size); } } 代码\n链接：https://pan.quark.cn/s/d943e41053e1\n提取码：XzdV\n","date":"2025-10-05T11:09:18Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%A2%E8%AF%95%E5%AE%98%E8%BF%BD%E9%97%AE%E7%9A%84%E5%A0%86%E6%8E%92%E5%BA%8F%E7%BB%86%E8%8A%82-%E8%BF%99%E7%AF%87%E8%AE%B2%E9%80%8F2%E7%A7%8D%E5%BB%BA%E5%A0%86%E6%96%B9%E5%BC%8F/","title":"面试官追问的堆排序细节-这篇讲透2种建堆方式"},{"content":" 本文采用随机选择算法来解决这道题。\n题目要求找到数组 nums 中第 k 大的元素。\n我们可以将问题转化为找到数组中第 nums.length - k 小的元素。\n1 2 3 public static int findKthLargest(int[] nums,int k){ return randomizedSelect(nums,nums.length-k); } 选择的核心逻辑 每次随机选一个 pivot，然后做三路划分： • 如果目标索引在左边，就往左区间继续找 • 如果在右边，就往右区间找 • 如果刚好落在等于 pivot 的区域，直接返回\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static int randomizedSelect(int[] arr,int i){ int ans=0; for(int l=0,r=arr.length-1;l\u0026lt;=r;){ partition(arr,l,r,arr[l+(int)(Math.random()*(r-l+1))]); if(i\u0026lt;first){ r=first-1; }else if(i\u0026gt;last){ l=last+1; }else{ ans = arr[i]; break; } } return ans; } 三路划分 把数组分成三块：\n[l, first): 小于 pivot [first, last]: 等于 pivot (last, r]: 大于 pivot\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static void partition(int[] arr, int l, int r, int x) { first = l; // 小于x区域的右边界 last = r; // 大于x区域的左边界 int i = l; while(i \u0026lt;= last) { if(arr[i] == x) { i++; // 等于就跳过 } else if(arr[i] \u0026lt; x) { swap(arr, first++, i++); // 小于就扔左边 } else { swap(arr, i, last--); // 大于就扔右边 } } } 代码 链接：https://pan.quark.cn/s/db66ccaf0511 提取码：62Vk\n","date":"2025-10-04T11:45:06Z","permalink":"https://iamxurulin.github.io/p/%E6%B1%82%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E7%AC%ACk%E4%B8%AA%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0/","title":"求数组中的第K个最大元素"},{"content":" 这道题如果用暴力的解法，对每一个元素遍历它左边的所有元素，累加比它更小的数，代码逻辑当然非常简单，但是时间复杂度会达到$O(n^2)$，如果数组的长度n达到1e5时，我们估算一下啊，1e10次的运算很明显会超时，这么做会通不过测试系统。\n而借助归并排序的思想，是可以将这道题的时间复杂度优化到$O(nlogn)$的。\n下面介绍代码逻辑：\n1.首先定义全局变量用于存储数据与临时数组\n1 2 3 4 public static int MAXN = 100001; // 数组最大长度 public static int[] arr = new int[MAXN]; // 存储原始数组 public static int[] help = new int[MAXN]; // 合并时的临时数组 public static int n; // 数组实际长度 2.输入输出的处理\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static void main(String[] args)throws IOException{ // 读入 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StreamTokenizer in = new StreamTokenizer(br); // 输出 PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 处理多组输入（直到文件结束） while(in.nextToken()!=StreamTokenizer.TT_EOF){ n = (int) in.nval; // 读取数组长度 // 读取数组元素 for(int i=0;i\u0026lt;n;i++){ in.nextToken(); arr[i]=(int)in.nval; } // 计算小和并输出 out.println(smallSum(0,n-1)); } out.flush(); // 刷新缓冲区，确保输出 out.close(); // 关闭输出流 } 3.分治函数\n1 2 3 4 5 6 7 8 public static long smallSum(int l,int r){ if(l==r){ // base case return 0; } int m = l+((r-l)\u0026gt;\u0026gt;1); // 中间位置，将数组分成 [l,m] 和 [m+1,r] // 分治逻辑：左半部分小和 + 右半部分小和 + 合并时的小和 return smallSum(l,m) + smallSum(m+1,r) + merge(l,m,r); } 4.合并函数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public static long merge(int l,int m,int r){ long ans = 0; // 存储当前合并阶段的小和 // 统计小和 for(int j=m+1, i=l, sum=0; j\u0026lt;=r; j++){ // 累积左数组 L 中比当前 R[j] 小的元素和 while(i\u0026lt;=m \u0026amp;\u0026amp; arr[i] \u0026lt;= arr[j]){ sum += arr[i++]; } ans += sum; } // 合并两个有序子数组到 help 数组 int i = l; int a = l; int b = m+1; while(a \u0026lt;= m \u0026amp;\u0026amp; b \u0026lt;= r){ // 两个数组都没遍历完 // 取较小的元素放入 help help[i++] = arr[a] \u0026lt;= arr[b] ? arr[a++] : arr[b++]; } // 处理左数组剩余元素 while(a \u0026lt;= m){ help[i++] = arr[a++]; } // 处理右数组剩余元素 while(b \u0026lt;= r){ help[i++] = arr[b++]; } // 将 help 中的有序数组拷贝回 arr for(i=l; i\u0026lt;=r; i++){ arr[i] = help[i]; } return ans; } 代码链接 链接：小和 提取码：2JMV\n","date":"2025-10-03T18:37:41Z","permalink":"https://iamxurulin.github.io/p/%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F%E5%B7%A7%E8%A7%A3%E8%AE%A1%E7%AE%97%E6%95%B0%E7%BB%84%E7%9A%84%E5%B0%8F%E5%92%8C%E9%97%AE%E9%A2%98/","title":"归并排序巧解计算数组的小和问题"},{"content":"归并排序的本质是 “分治”：\n把大数组拆成小数组，各自排好序后，再合并成一个有序数组。\n就像把一堆乱牌分成两半，每半先理好，再把两堆理好的牌一张张比对，按顺序合成一堆 —— 这就是 “分” 和 “合” 的过程。\n下面看一道Leetcode题： 递归实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 public class Main { public static int[] sortArray(int[] nums){ // 数组长度大于1时才需要排序（长度为0或1默认有序） if(nums.length\u0026gt;1){ mergeSort(nums); } return nums; } // 定义最大数组长度 public static int MAXN = 50001; // 辅助数组：用于归并过程中临时存储元素，避免每次合并都新建数组 public static int[] help = new int[MAXN]; public static void mergeSort(int[] arr){ // 从数组的0号位置到最后一个位置（arr.length-1）进行排序 sort(arr,0,arr.length-1); } public static void sort(int[] arr,int l,int r){ // 当区间只有一个元素时（l==r），无需排序，直接返回 if(l==r){ return; } // 计算中间位置（等价于(l+r)/2，但用位运算\u0026gt;\u0026gt;1避免溢出，且效率更高） int m = l + ((r - l) \u0026gt;\u0026gt; 1); // 递归排序左半区间 [l, m] sort(arr,l,m); // 递归排序右半区间 [m+1, r] sort(arr,m+1,r); // 合并左右两个已排序的区间 merge(arr,l,m,r); } public static void merge(int[] arr,int l,int m,int r){ int i = l; // 辅助数组help的当前填充位置（从l开始） int a = l; // 左区间的遍历指针（从l开始） int b = m + 1; // 右区间的遍历指针（从m+1开始） // 1. 双指针遍历左右区间，将较小的元素依次放入help while(a \u0026lt;= m \u0026amp;\u0026amp; b \u0026lt;= r){ // 谁小就先放谁，相等时优先放左区间元素（保证稳定性） help[i++] = arr[a] \u0026lt;= arr[b] ? arr[a++] : arr[b++]; } // 2. 左区间还有剩余元素，全部放入help（右区间已遍历完） while(a \u0026lt;= m){ help[i++] = arr[a++]; } // 3. 右区间还有剩余元素，全部放入help（左区间已遍历完） while(b \u0026lt;= r){ help[i++] = arr[b++]; } // 4. 将help中排序好的元素拷贝回原数组的[l, r]区间 for(i = l; i \u0026lt;= r; i++){ arr[i] = help[i]; } } } 非递归实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public class Main { public static int[] sortArray(int[] nums){ if(nums.length\u0026gt;1){ mergeSort(nums); } return nums; } public static int MAXN = 50001; public static int[] help = new int[MAXN]; public static void mergeSort(int[] arr){ int n = arr.length; // step：当前要合并的子数组长度，初始为1（单个元素默认有序），每次翻倍（左移1位等价于×2） // 循环条件：step \u0026lt; n（当子数组长度超过数组长度时，排序完成） for(int step=1; step \u0026lt; n; step \u0026lt;\u0026lt;= 1){ int l = 0; // 每次合并的起始位置，从0开始 // 遍历数组，按当前step划分并合并子数组 while(l \u0026lt; n){ // m：左子数组的结束位置（左子数组范围 [l, m]，长度为step） int m = l + step - 1; // 若右子数组起始位置（m+1）已超出数组范围，说明只剩一个子数组，无需合并 if(m + 1 \u0026gt;= n){ break; } // r：右子数组的结束位置（右子数组最大长度为step，取较小值避免越界） int r = Math.min(l + (step \u0026lt;\u0026lt; 1) - 1, n - 1); // 合并 [l, m] 和 [m+1, r] 两个有序子数组 merge(arr, l, m, r); // 移动到下一组子数组的起始位置 l = r + 1; } } } public static void merge(int[] arr,int l,int m,int r){ int i=l; int a = l; int b = m+1; while(a\u0026lt;=m\u0026amp;\u0026amp;b\u0026lt;=r){ help[i++]=arr[a]\u0026lt;=arr[b]?arr[a++]:arr[b++]; } while(a\u0026lt;=m){ help[i++]=arr[a++]; } while(b\u0026lt;=r){ help[i++]=arr[b++]; } for(i=l;i\u0026lt;=r;i++){ arr[i]=help[i]; } } } 代码文件 链接：https://pan.quark.cn/s/fc867494224a 提取码：9nSb\n","date":"2025-10-02T12:59:31Z","permalink":"https://iamxurulin.github.io/p/%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F%E7%9A%84%E9%80%92%E5%BD%92%E5%92%8C%E9%9D%9E%E9%80%92%E5%BD%92%E5%AE%9E%E7%8E%B0/","title":"归并排序的递归和非递归实现"},{"content":" 在算法笔试中，一般有两种风格，一种是填函数风格，可以粗暴地理解为Leetcode中提交的风格，另一种是ACM风格，可以认为是要自己写输入输出的风格，比如华为笔试的风格。\n如果是ACM风格，尽量不要使用Scanner、System.out，因为他们的IO效率非常慢。\nACM风格也有两种常见的形式，一种是给定数据规模，另一种是未给定数据规模。\n给定数据规模 这道子矩阵的最大累加和问题是一道典型的给定数据规模的题目。\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public static void main(String[] args) throws IOException{ //将文件中的内容load进内存中 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); //一个一个读数字 StreamTokenizer in = new StreamTokenizer(br); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); //文件没有结束就继续 while(in.nextToken()!=StreamTokenizer.TT_EOF){ int n = (int)in.nval; in.nextToken(); int m = (int)in.nval; int[][] mat = new int[n][m]; for(int i=0;i\u0026lt;n;i++){ for(int j = 0;j\u0026lt;m;j++){ in.nextToken(); mat[i][j]=(int)in.nval; } } out.println(maxSumSubmatrix(mat,n,m)); } out.flush(); br.close(); out.close(); } public static int maxSumSubmatrix(int[][] mat,int n,int m){ int max = Integer.MIN_VALUE; for (int i=0;i\u0026lt;n;i++){ int[] arr = new int[m]; for(int j=i;j\u0026lt;n;j++){ for(int k=0;k\u0026lt;m;k++){ arr[k]+=mat[j][k]; } max = Math.max(max,maxSumSubarray(arr,m)); } } return max; } public static int maxSumSubarray(int[] arr,int m){ int max = Integer.MIN_VALUE; int cur = 0; for(int i=0;i\u0026lt;m;i++){ cur+=arr[i]; max=Math.max(max,cur); cur = cur\u0026lt;0?0:cur; } return max; } 未给定数据规模 对于没有给定数据规模的ACM风格的笔试题，其实这个时候你想用Scanner也用不了，因为你不知道用户会输入几个数字，所以这个时候可以考虑按行读取。\n代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 // 静态变量：用于存储读取的行和分割后的数字数组 public static String line; public static String[] parts; public static int sum; public static void main(String[] args) throws IOException { // 1. 创建输入流：从标准输入（键盘）读取数据 BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); // 2. 创建输出流：向标准输出（控制台）写入数据 PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); // 3. 循环读取每行数据，直到输入结束（如 Ctrl+D 或文件末尾） while ((line = in.readLine()) != null) { // 4. 按空格分割当前行，得到数字字符串数组 parts = line.split(\u0026#34; \u0026#34;); sum = 0; // 重置求和变量 // 5. 遍历数组，将每个字符串转换为整数并累加 for (String num : parts) { sum += Integer.valueOf(num); } // 6. 输出当前行的求和结果 out.println(sum); } // 7. 资源清理：刷新输出流并关闭所有流 out.flush(); in.close(); out.close(); } PS\n1.BufferedReader 和 PrintWriter 内部维护缓冲区，减少了直接操作磁盘或控制台的次数（IO 操作是程序性能瓶颈之一），尤其在处理大量数据时优势明显。\n2.循环读取 line != null 的逻辑适用于各种场景（如读取文件、网络流、控制台输入），只需替换输入流的来源即可复用代码。\n","date":"2025-09-30T21:27:03Z","permalink":"https://iamxurulin.github.io/p/%E4%B8%A4%E7%A7%8D%E5%B8%B8%E8%A7%81%E7%9A%84acm%E9%A3%8E%E6%A0%BC%E7%AC%94%E8%AF%95%E9%A2%98/","title":"两种常见的ACM风格笔试题"},{"content":"本文约定用“打印”这个术语来表示执行某个操作。\n用两个栈来实现 先头节点入栈，然后弹出节点但是接着不打印，而是再次放到一个收集栈里，然后先压左子树再压右子树，这样原本弹出栈的顺序应该是根右左，但是经过这个收集栈之后就变成了左右根，这样的话就能够实现后序遍历。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static List\u0026lt;Integer\u0026gt; postorderTraversal(TreeNode head) { List\u0026lt;Integer\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); if(head!=null){ Stack\u0026lt;TreeNode\u0026gt; stack = new Stack\u0026lt;\u0026gt;(); Stack\u0026lt;TreeNode\u0026gt; collect = new Stack\u0026lt;\u0026gt;(); stack.push(head); while(!stack.isEmpty()){ head = stack.pop(); collect.push(head); if(head.left!=null){ stack.push(head.left); } if(head.right!=null){ stack.push(head.right); } } while (!collect.isEmpty()){ ans.add(collect.pop().val); } } return ans; } 用一个栈来实现 刚开始，我们用h来表示头节点，如果始终没有打印过节点，则h就一直是头节点。 一旦打印过节点，则h就变成上一次打印的节点。 如果有左子树并且左子树没有处理过，则左子树进栈。 如果有右子树并且右子树没有处理过，则右子树进栈。 如果左子树、右子树为空或者都处理过了，则打印并将h指向弹出的节点。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static List\u0026lt;Integer\u0026gt; postorderTraversal(TreeNode h) { List\u0026lt;Integer\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); if(h!=null){ Stack\u0026lt;TreeNode\u0026gt; stack = new Stack\u0026lt;\u0026gt;(); stack.push(h); while(!stack.isEmpty()){ TreeNode cur = stack.peek(); if(cur.left!=null\u0026amp;\u0026amp;h!=cur.left\u0026amp;\u0026amp;h!=cur.right){ stack.push(cur.left); }else if(cur.right!=null\u0026amp;\u0026amp;h!=cur.right){ stack.push(cur.right); }else{ ans.add(cur.val); h=stack.pop(); } } } return ans; } ","date":"2025-09-29T21:27:09Z","permalink":"https://iamxurulin.github.io/p/%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%90%8E%E5%BA%8F%E9%81%8D%E5%8E%86-%E9%9D%9E%E9%80%92%E5%BD%92%E7%89%88/","title":"二叉树的后序遍历-非递归版"},{"content":"\n二叉树的中序遍历 代码实现逻辑:\n1)来到某一个子树的头节点，整条左边界进栈，一直到整条左边界遍历完；\n2)从栈中弹出节点，打印（这里的打印是指可以做某个动作，比如此处是将节点值添加到结果链表），把弹出打印节点的右子树看作是新的子树的头节点，然后重复步骤1）； 3)当没有子树并且栈为空了，结束\n注意如果右子树为空，可以假定它执行了比如步骤1)但是什么都没有干成，这样更好理解一些。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static List\u0026lt;Integer\u0026gt; inorderTraversal(TreeNode head) { List\u0026lt;Integer\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); if(head!=null){ Stack\u0026lt;TreeNode\u0026gt; stack = new Stack\u0026lt;\u0026gt;(); while(!stack.isEmpty()||head!=null){ if(head!=null){ stack.push(head); head = head.left; }else { head = stack.pop(); ans.add(head.val); head= head.right; } } } return ans; } ","date":"2025-09-28T20:40:28Z","permalink":"https://iamxurulin.github.io/p/%E9%9D%9E%E9%80%92%E5%BD%92%E5%AE%9E%E7%8E%B0%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86/","title":"非递归实现二叉树的中序遍历"},{"content":"首先，电脑的配置是Windows 11 专业版+VMware Workstation Pro 15.5.\n1.因为开启了Hyper -V，所以会出现如下问题： 根据网上查找到的一些解决方案，将Hyper -V和Device Guard这些禁用掉，以为可以解决这个问题，然而。。。。。。 出现如下问题2.。\n2.Your device ran into a problem and needs to restart. We\u0026rsquo;re just collecting some error info, and then you can restart. 这个问题直白一点就是蓝屏，但是不知道为什么此时显示黑屏。。。。\n于是，我感觉是Windows 11 又更新或者删除了什么东西导致了这些所谓的乱七八糟的问题。\n多次尝试网上的解决方案之后仍然没有解决该问题。\n当事人十分后悔前几天把电脑更新到了Windows 11，于是决定重新装回稳定一点的Windows 10，但是，等等，重装系统之后又要重新装软件，太麻烦了。\n灵机一动，将这个问题喂给豆包，得到的回复中有一条是：“旧版本（如 VMware Workstation Pro 15 及以下）对 Windows 11 兼容性较差，可能触发蓝屏。”\n于是，赶紧把VMware Workstation Pro 15.5给卸载了，重新装了个17.6，你猜怎么着？\n没错，问题解决了。\n","date":"2025-09-07T23:11:14Z","permalink":"https://iamxurulin.github.io/p/windows-11%E9%80%9A%E8%BF%87vmware-workstation-pro%E6%90%AD%E5%BB%BAcentos7.6%E7%B3%BB%E7%BB%9F%E9%81%87%E5%88%B0%E7%9A%84%E9%97%AE%E9%A2%98/","title":"Windows-11通过VMware-Workstation-Pro搭建centos7.6系统遇到的问题"}]