LeetCode热题100(Java)(7)链表(下)
本章包括的题目有24. 两两交换链表中的节点 - 力扣LeetCode25. K 个一组翻转链表 - 力扣LeetCode138. 随机链表的复制 - 力扣LeetCode148. 排序链表 - 力扣LeetCode23. 合并 K 个升序链表 - 力扣LeetCode146. LRU 缓存 - 力扣LeetCode1.两两交换链表中的节点思路解析这道题的难点在于两两交换时指针容易丢失尤其是头节点也会参与交换直接操作很容易出错。代码采用了一个比较稳妥的策略先把每一对中靠前的节点按顺序存进数组把链表的动态结构拍平成静态的索引关系后续所有指针修改都基于数组下标进行避免了边遍历边改指针带来的混乱。具体来说第一轮遍历只收集每对的起始节点第二轮遍历则根据数组中的位置关系统一处理交换和拼接当前对的第二个节点指向第一个节点完成内部交换而第一个节点的 next 则根据下一对是否存在来决定接谁——如果还有下一对就接下一对的第二个节点即arr[i1].next否则接下一对的第一个节点或置空。这样无论链表长度是奇数还是偶数逻辑都是统一的不需要为边界情况写额外分支。最后返回预先保存的原第二个节点作为新头因为头节点一定参与了第一次交换这个值就是最终结果。代码实现class Solution { public ListNode swapPairs(ListNode head) { ListNode cur head; if(head null) return null; ListNode nex head.next; if(nex null) return head; ArrayListListNode arr new ArrayList(); while(cur ! null){ arr.add(cur); if(cur.next ! null) cur cur.next.next; else break; } for(int i 0;i arr.size();i){ if(arr.get(i).next ! null){ arr.get(i).next.next arr.get(i); if(i 1 arr.size()){ if(arr.get(i1).next ! null) arr.get(i).next arr.get(i1).next; else arr.get(i).next arr.get(i1); }else arr.get(i).next null; } } return nex; } }时间复杂度On空间复杂度On2.K 个一组翻转链表思路解析这道题的核心难点在于反转操作不可逆必须先确认当前分组恰好有 k 个节点才能执行否则不足 k 个的尾部需要保持原序。因此我将验证分组长度与执行反转解耦为两个独立步骤避免反转后发现长度不足而无法回退的问题。具体实现上每轮循环首先让 curTail 向前移动 k-1 步进行探测。若中途遇到 null说明剩余节点不足 k 个此时直接返回首次记录的头节点 firstTail因为已处理部分的头不会再改变。仅当 curTail 有效时才将 curTail.next 置空把当前 k 个节点从主链上切离并调用 swapList 完成反转。该函数职责单一仅负责将传入链表头尾颠倒并返回新头即原尾不涉及外部上下文。反转完成后原头节点变为新尾 newTail需将其与后续分组衔接。下一分组的起始节点为 nextHead但同样需要先验证其长度是否达到 k。通过让 nextTail 再走 k-1 步探测若探测失败则将 newTail 直接接上 nextHead保持原始顺序并返回 firstTail若探测成功则将 newTail 接至 nextTail并更新 curHead 与 curTail 进入下一轮迭代。这种设计使主逻辑聚焦于探测→切断→反转→衔接的流程控制反转细节被封装在独立函数中降低了耦合度。空间上仅使用常数个指针变量为 O(1) 原地操作时间上每个节点最多被访问两次探测一次、反转一次总体复杂度 O(n)在保证正确性的前提下兼顾了代码的清晰度与可维护性。代码实现class Solution { public ListNode reverseKGroup(ListNode head, int k) { ListNode curHead head; ListNode curTail head; for (int i 1; i k; i) { curTail curTail.next; if(curTail null) return head; } ListNode firstTail curTail; while(true){ if(curTail null)return firstTail; ListNode nextHead curTail.next; curTail.next null; //断开和后面的连接 ListNode newTail swapList(curHead); ListNode nextTail nextHead; if(nextTail null) { newTail.next nextHead; return firstTail; } //寻找下一个头和尾 for (int i 1; i k; i) { nextTail nextTail.next; if(nextTail null) { newTail.next nextHead; return firstTail; } } newTail.next nextTail; curTail nextTail; curHead nextHead; } } //反转链表的函数 private ListNode swapList(ListNode head){ if(head null || head.next null) return head; ListNode pre null; ListNode cur head; ListNode nex cur.next; while(cur ! null){ cur.next pre; pre cur; cur nex; if(nex ! null)nex nex.next; } return head; } }时间复杂度On空间复杂度O13.随机链表的复制思路解析这道题的难点在于 random 指针可能指向链表中任意节点甚至为 null若边创建节点边设置指针很容易遇到目标节点尚未创建的情况。因此我采用两遍遍历的策略将节点创建与指针连接彻底分离确保第二遍设置指针时所有新节点都已存在。第一遍遍历仅负责建立原节点到新节点的映射关系。遍历原链表对每个节点创建一个值相同的新节点并以原节点为键、新节点为值存入 HashMap。此阶段不设置任何指针只保证每个原节点都有对应的新节点副本。第二遍遍历专注于指针的赋值。再次遍历原链表对每个原节点 cur通过 map.get(cur) 获取其对应的新节点然后利用 map.get(cur.next) 和 map.get(cur.random) 直接获取新节点的 next 与 random 指向的目标。由于 HashMap 的 get 方法在键为 null 时返回 null恰好天然处理了 next 或 random 为空的情况无需额外判断。这种分步策略的优势在于逻辑清晰且避免了重复查找或临时标记。空间上使用了 O(n) 的哈希表存储映射关系时间上两次线性遍历均为 O(n)总体 O(n)。相比在原链表节点间插入新节点再拆分的原地做法该方法以空间换取了代码的可读性与实现的简洁性更易于理解和维护。代码实现class Solution { public Node copyRandomList(Node head) { if (head null) return null; // 第一次遍历创建新节点并建立原节点 - 新节点的映射 MapNode, Node map new HashMap(); Node cur head; while (cur ! null) { map.put(cur, new Node(cur.val)); cur cur.next; } // 第二次遍历设置 next 和 random 指针 cur head; while (cur ! null) { Node newNode map.get(cur); newNode.next map.get(cur.next); // 可能为 nullmap.get(null) 返回 null newNode.random map.get(cur.random); // 同理 cur cur.next; } return map.get(head); } }时间复杂度On空间复杂度On4.排序链表思路解析本解法采用自底向上的迭代归并排序核心目标是在保证 O(n log n) 时间复杂度的前提下将空间复杂度严格控制在 O(1)。整体逻辑由主排序流程驱动并通过切割与合并两个辅助操作协同完成。算法首先遍历链表获取总长度作为控制合并轮次的终止条件。随后引入虚拟头节点指向原链表头以此统一每轮合并后的拼接操作消除对头节点的特殊判断。外层循环以步长从 1 开始逐轮倍增当步长达到或超过链表总长度时终止此时整个链表已有序。每一轮内部通过前驱指针与当前指针的配合依次处理所有相邻的子链表对先从当前位置切出指定步长的左子链表并获得右子链表起点再从该起点切出同等长度的右子链表并获得下一对的起始位置接着将左右两部分合并后接回主链表最后将前驱指针移至已合并段的末尾为下一次拼接做准备。切割操作被封装为独立函数其职责是从给定头节点出发截取最多指定数量的节点并返回剩余部分的起始位置。实现时向后移动相应步数若中途遇到空节点则提前终止随后断开当前节点的后续链接并返回原后续指针。这一设计使得不足指定长度的边界情况无需额外处理切割与定位一步完成。合并操作同样独立封装采用经典的双指针归并策略。通过局部虚拟头节点构建结果链表比较两链表当前节点值并将较小者接入尾部直至一方耗尽后直接拼接另一方剩余部分。该函数不依赖任何外部状态每次调用产生的局部变量在返回后自动释放不会累积额外空间开销。代码实现class Solution { public ListNode sortList(ListNode head) { if (head null || head.next null) return head; // 1. 统计链表长度 int len 0; for (ListNode cur head; cur ! null; cur cur.next) len; ListNode dummy new ListNode(0, head); // 2. 按步长 step 1, 2, 4, ... 逐轮合并 for (int step 1; step len; step 1) { ListNode prev dummy; ListNode curr dummy.next; while (curr ! null) { // 切出左半部分最多 step 个节点 ListNode left curr; ListNode right split(left, step); // 切出右半部分最多 step 个节点并返回下一段的起始位置 curr split(right, step); // 合并左右两部分并接回主链表 prev.next merge(left, right); // 移动 prev 到已合并段的末尾 while (prev.next ! null) prev prev.next; } } return dummy.next; } /** * 从 head 开始切出最多 n 个节点返回第 n1 个节点即下一段起点 * 同时将切出的子链表尾部 next 置空 */ private ListNode split(ListNode head, int n) { for (int i 1; head ! null i n; i) { head head.next; } if (head null) return null; ListNode next head.next; head.next null; // 断开 return next; } /** * 合并两个有序链表返回新头 */ private ListNode merge(ListNode l1, ListNode l2) { ListNode dummy new ListNode(0); ListNode tail dummy; while (l1 ! null l2 ! null) { if (l1.val l2.val) { tail.next l1; l1 l1.next; } else { tail.next l2; l2 l2.next; } tail tail.next; } tail.next (l1 ! null) ? l1 : l2; return dummy.next; } }时间复杂度Onlogn空间复杂度On5.合并 K 个升序链表思路解析面对 K 个有序链表的合并问题最直接的暴力做法是反复扫描所有链表头找最小值但每轮扫描耗时 O(K)总代价 O(NK) 显然不够高效。既然每次只需要当前全局最小自然联想到用最小堆来维护这个动态候选集堆中始终存放每条链表尚未处理的头节点取堆顶即得全局最小取出后若该链表还有后继便将后继补入堆中继续参与竞争。这样每次操作仅 O(log K)N 个节点总计 O(N log K)且堆的大小恒为 K空间 O(K)。基于这一思路代码实现分为三步。第一步是初始化创建以节点值为序的最小堆遍历输入数组将所有非空头节点入堆同时设立虚拟头节点作为结果链表的构建锚点。第二步是主循环只要堆非空就弹出堆顶节点接入结果链表尾部并检查其后继是否存在存在则入堆这一步将取最小与补后继合并在同一轮迭代中完成保证堆内始终是各链表当前的有效候选。第三步是返回虚拟头的下一个节点作为真正的结果头。代码实现public class Solution { public ListNode mergeKLists(ListNode[] lists) { PriorityQueueListNode pq new PriorityQueue((l1,l2)-(l1.val - l2.val)); for(ListNode l : lists){ if(l ! null) pq.add(l); } ListNode v new ListNode(0); ListNode pre v; while(!pq.isEmpty()){ ListNode cur pq.poll(); if(cur.next ! null) pq.add(cur.next); pre.next cur; pre pre.next; } return v.next; } }时间复杂度Onlogk空间复杂度Ok6. LRU 缓存思路解析设计 LRU 缓存的核心挑战在于同时满足两个操作均为 O(1)按 key 快速查找以及按访问时间顺序快速调整节点位置。哈希表天然支持 O(1) 查找但无法维护顺序链表能维护顺序但查找需线性扫描。因此将两者结合哈希表以 key 为键、节点引用为值实现即时定位双向链表按访问新旧排列节点最近使用的靠近尾部最久未使用的靠近头部插入、删除、移动均只需修改相邻指针无需遍历。为进一步消除边界判断引入虚拟头尾节点。它们不存储有效数据仅作为链表的固定锚点使得在尾部添加删除指定节点获取头部真实节点等操作无需区分空链表或单节点等特殊情况代码逻辑统一且不易出错。基于这一数据结构组合get 操作的流程是先通过哈希表查找 key 对应的节点若不存在直接返回 -1若存在则将该节点从当前位置摘除并重新接到链表尾部标记为最近使用再返回其 value。put 操作则分两种情况若 key 已存在更新节点值并将其移至尾部若 key 不存在需先检查容量是否已满满时则删除链表头部即 head.next所指向的真实节点同步从哈希表中移除对应条目并减少计数然后创建新节点加入尾部、写入哈希表并增加计数。整个过程中哈希表与链表的状态始终保持一致任何一方的变更都伴随另一方的同步更新确保查找与顺序维护的正确性。代码实现class LRUCache { class Node { int key, value; Node prev, next; Node(int k, int v) { key k; value v; } } private MapInteger, Node cache; private Node head, tail; private int capacity; private int size; public LRUCache(int capacity) { this.capacity capacity; this.size 0; cache new HashMap(); // 虚拟头尾节点简化边界操作 head new Node(0, 0); tail new Node(0, 0); head.next tail; tail.prev head; } private void addToTail(Node node) { node.prev tail.prev; node.next tail; tail.prev.next node; tail.prev node; } private void removeNode(Node node) { node.prev.next node.next; node.next.prev node.prev; } private void moveToTail(Node node) { removeNode(node); addToTail(node); } public int get(int key) { Node node cache.get(key); if (node null) return -1; moveToTail(node); // 更新为最近使用 return node.value; } public void put(int key, int value) { Node existing cache.get(key); if (existing ! null) { existing.value value; // 更新值 moveToTail(existing); } else { if (size capacity) { // 删除最久未使用的节点head.next Node lru head.next; cache.remove(lru.key); removeNode(lru); size--; } Node newNode new Node(key, value); cache.put(key, newNode); addToTail(newNode); size; } } }

相关新闻

Linux面试Top100:从命令到原理,构建系统性知识图谱

Linux面试Top100:从命令到原理,构建系统性知识图谱

1. 项目概述:一份“精通”Linux的底气从何而来看到这个标题,我仿佛看到了屏幕后面那个自信满满、准备在面试中大杀四方的你。确实,在技术面试,尤其是后端、运维、SRE、嵌入式等岗位的面试中,Linux知识是绕不开的硬通货…

2026/9/18 8:01:47 阅读更多 →
防火墙概述

防火墙概述

防火墙定义防火墙是一种网络安全设备,它按照预定的安全规则,控制跨网络边界的流量转发,并对流量进行内容安全一体化检测。基本原理基于安全策略(本质是包过滤规则)对流量进行匹配,决定是“允许通过”、“拒…

2026/9/2 17:11:25 阅读更多 →
Excel从入门到精通:核心函数、数据透视表与高效数据处理实战指南

Excel从入门到精通:核心函数、数据透视表与高效数据处理实战指南

你是不是也遇到过这样的场景:领导甩过来一个Excel表格,要求“半小时内把销售数据按区域和产品线汇总一下,顺便做个趋势图”,而你盯着密密麻麻的数据,大脑一片空白,只能硬着头皮一个个手动筛选、复制粘贴&am…

2026/9/21 6:54:37 阅读更多 →

最新新闻

DeepSeek Harness 0.1.6-alpha.2:本地多智能体编排与插件管理实践指南

DeepSeek Harness 0.1.6-alpha.2:本地多智能体编排与插件管理实践指南

如果你最近在折腾本地大模型,大概率已经听说过 DeepSeek Harness 这个名字。它不是一个单纯的模型调用脚本,而是一个把本地模型、外部工具、多个智能体整合到一起的调度框架。0.1.6-alpha.2 这个版本号看起来很小,但内核变化并不小——官方插…

2026/9/24 20:31:48 阅读更多 →
LaTeX转Word公式乱码与排版崩溃全解决:Pandoc与ai2word实战指南

LaTeX转Word公式乱码与排版崩溃全解决:Pandoc与ai2word实战指南

1. 学术论文格式转换的核心痛点与方案选型1.1 为什么LaTeX转Word是个高频刚需学术论文写作圈子里有个心照不宣的事实:投稿用LaTeX,交稿用Word。很多期刊的投稿系统只接受PDF,但导师改稿、合作者批注、盲审返回意见,往往要求Word版…

2026/9/24 20:31:48 阅读更多 →
2026年Data Agent本地部署选型指南:开源、自建与企业级路线全解析

2026年Data Agent本地部署选型指南:开源、自建与企业级路线全解析

1. 为什么2026年Data Agent本地部署突然成了刚需1.1 从“能用”到“敢用”的转折点2024年之前,大部分团队对Data Agent的态度是“先跑通再说”,数据往云端一扔,API一调,能出结果就行。但到了2025年下半年,情况明显变了…

2026/9/24 20:31:48 阅读更多 →
企业级CI/CD流水线选型:从能跑通到强管控与信创适配

企业级CI/CD流水线选型:从能跑通到强管控与信创适配

1. 从“能跑通”到“强管控”:一个老兵的流水线选型观做了十多年交付和平台工程,我参与过不下三十次CI/CD选型。最常听到的一句话就是:“我们先用Jenkins把流程跑通再说。”这句话本身没错,但问题在于,很多团队跑通之后…

2026/9/24 20:31:48 阅读更多 →
LaTeX转Word全攻略:Pandoc转换公式与参考文献实操指南

LaTeX转Word全攻略:Pandoc转换公式与参考文献实操指南

1. 学术写作工具链的现实困境与破局思路1.1 为什么 LaTeX 到 Word 的转换成了刚需做科研的人大概都经历过这种场景:论文投稿时期刊要求 LaTeX 源文件,导师改稿却只认 Word 的批注功能,或者合作方直接甩过来一句“你发个 Word 版给我&#xff…

2026/9/24 20:31:48 阅读更多 →
Word内容粘贴到富文本编辑器样式丢失?一套清洗管线方案彻底解决

Word内容粘贴到富文本编辑器样式丢失?一套清洗管线方案彻底解决

1. 问题根源:为什么Word内容一进浏览器就“变脸”做前端的人,尤其是跟CMS后台、富文本编辑器、协同文档打过交道的,基本都遇到过一个让人抓狂的场景:客户或者运营同事在Word里排版排得漂漂亮亮,标题带色、段落缩进、表…

2026/9/24 20:30:47 阅读更多 →

日新闻

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为…

2026/9/24 0:00:19 阅读更多 →
单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

简介:一份基于单细胞RNA测序数据的细胞类型注释算法研究Python毕业设计源码,针对计算机相关专业正在做毕设或需要项目实战的学习者,可用于课程设计与期末大作业。项目代码完整、经导师指导评审通过,可直接运行,覆盖数据…

2026/9/24 0:00:19 阅读更多 →
C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

第一次在项目里被反射卡住,是在一个老旧的WinForms模块里:几十个类依赖PropertyChanged通知,运行时反射读属性、发通知,每次启动慢半拍不说,一上.NET Native/AOT裁剪模式几乎全面崩盘。后来我把这段逻辑全部改成C#源生…

2026/9/24 0:00:19 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/24 14:34:13 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/24 9:10:42 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/24 14:33:56 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/24 12:50:34 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/24 14:33:48 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/24 12:49:17 阅读更多 →