LeetCode-Hot100 31.K个一组反转链表

【保姆级详解】K 个一组翻转链表(迭代解法)

K个一组翻转链表 是链表操作类算法题中的经典进阶题,它不仅融合了「链表反转」的核心逻辑,还需要处理分组、边界拼接等复杂场景。本文会从问题拆解、核心思路、代码逐行解析、执行流程模拟四个维度,把这道题的迭代解法讲透,方便你复习时快速理解每一个细节。

一、问题描述

题目要求

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

  • k 是一个正整数,它的值小于或等于链表的长度。
  • 如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
  • 要求:不能只是单纯改变节点内部的值,而是需要实际进行节点交换。

示例

  • 输入:head = [1,2,3,4,5], k = 2
  • 输出:[2,1,4,3,5]
  • 输入:head = [1,2,3,4,5], k = 3
  • 输出:[3,2,1,4,5]

链表节点定义

/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ 

二、核心思路拆解

这道题的核心是「分组反转 + 边界拼接」,可以拆解为 3 个关键步骤:

  1. 统计链表长度:确定能分成多少个完整的 k 节点组(避免对不足 k 个的最后一组进行反转);
  2. 虚拟头节点兜底:用 dummy 节点统一 “首组反转” 和 “后续组反转” 的拼接逻辑,避免单独处理头节点;
  3. 分组反转 + 拼接
    • 对每一组 k 个节点执行「链表反转」操作;
    • 把反转后的组与前一组、后一组正确拼接;
    • 更新指针,处理下一组。

三、完整代码(带详细注释)

class Solution { public ListNode reverseKGroup(ListNode head, int k) { // 步骤1:统计链表总长度,用于判断能分成多少个完整的k组 int n = 0; for(ListNode cur = head; cur != null; cur = cur.next) { n++; } // 步骤2:创建虚拟头节点,指向原头节点(统一边界处理) ListNode dummy = new ListNode(0, head); // p0:每一组待反转节点的「前驱节点」(关键!用于拼接反转后的组) ListNode p0 = dummy; // pre/cur:链表反转的经典双指针(pre初始为null,cur初始为头节点) ListNode pre = null; ListNode cur = head; // 步骤3:遍历处理每一个k组(剩余节点数≥k时才反转) for(; n >= k; n -= k) { // 步骤4:反转当前k个节点(标准的「链表反转」逻辑) for(int i = 0; i < k; i++) { ListNode nxt = cur.next; // 保存cur的下一个节点,避免丢失 cur.next = pre; // 反转cur的指向(指向pre) pre = cur; // pre后移 cur = nxt; // cur后移 } // 步骤5:拼接反转后的组(核心!这一步是易错点) ListNode nxt = p0.next; // 保存当前组反转前的头节点(反转后变成组尾) p0.next.next = cur; // 让反转后的组尾指向cur(下一组的头/剩余未反转节点) p0.next = pre; // 让前驱节点p0指向反转后的组头(pre) p0 = nxt; // 更新p0为当前组的组尾,作为下一组的前驱 } // 步骤6:返回虚拟头节点的下一个节点(反转后的新头) return dummy.next; } } 

四、逐行代码解析(复习重点)

1. 统计链表长度

int n = 0; for(ListNode cur = head; cur != null; cur = cur.next) { n++; } 
  • 作用:计算链表总节点数 n,后续通过 n >= k 判断是否需要反转当前组(比如 n=5, k=2 时,能反转 2 组,剩余 1 个节点不处理);
  • 细节:遍历过程中不修改原链表,仅做计数,时间复杂度 O(n)

2. 初始化核心指针

ListNode dummy = new ListNode(0, head); ListNode p0 = dummy; ListNode pre = null; ListNode cur = head; 
指针初始值核心作用
dummy指向原头节点避免单独处理 “第一组反转后头节点变更” 的问题,最终返回 dummy.next 即可;
p0指向 dummy每一组待反转节点的前驱节点,负责把反转后的组拼接到前序链表中;
prenull链表反转的 “前一个节点”,初始为 null(符合链表反转的初始逻辑);
cur原头节点 head链表反转的 “当前节点”,从原头节点开始遍历。

3. 分组反转的外层循环

for(; n >= k; n -= k) { ... } 
  • 循环条件n >= k 表示剩余节点数足够组成一个 k 组,才执行反转;
  • n -= k:每处理完一组,剩余节点数减少 k,直到不足 k 个时停止。

4. 反转当前 k 个节点(经典链表反转)

for(int i = 0; i < k; i++) { ListNode nxt = cur.next; cur.next = pre; pre = cur; cur = nxt; } 
  • 核心逻辑:标准的 “原地反转链表”,循环 k 次仅反转当前组的 k 个节点;
  • 关键变量nxt 必须提前保存 cur.next,否则反转 cur.next = pre 后会丢失后续节点;
  • 执行后状态
    • pre:指向当前组反转后的组头
    • cur:指向下一组的第一个节点(或剩余未反转节点)。

5. 拼接反转后的组(最易错!)

ListNode nxt = p0.next; // ① p0.next.next = cur; // ② p0.next = pre; // ③ p0 = nxt; // ④ 

这 4 行是整个代码的核心难点,我们用 head=[1,2,3,4,5], k=2 的第一组反转(1→2)为例拆解:

  • ListNode nxt = p0.nextp0 初始指向 dummyp0.next1(反转前的组头,反转后变成组尾),用 nxt 保存这个组尾;
  • p0.next.next = curp0.next1cur 此时指向 3(下一组的头),这一步让反转后的组尾 1 指向 3,避免链表断裂;
  • p0.next = prepre 此时指向 2(反转后的组头),这一步让 p0dummy)指向 2,完成第一组的拼接(dummy→2→1→3→4→5);
  • p0 = nxt:把 p0 更新为 1(当前组的组尾),作为下一组(3→4)的前驱节点,准备处理下一组。

6. 返回结果

return dummy.next; 
  • 无论第一组是否反转,dummy.next 始终指向最终链表的头节点(避免返回原头节点导致错误)。

五、执行流程模拟(以 head=[1,2,3,4,5], k=2 为例)

为了方便复习时快速回忆,我们模拟完整执行步骤:

步骤指针状态链表状态
初始状态n=5, dummy→1→2→3→4→5, p0=dummy, pre=null, cur=1dummy→1→2→3→4→5
第一组反转(i=0/1)反转后 pre=2, cur=31←2 3→4→5(临时断裂)
第一组拼接nxt=1, 1→3, dummy→2, p0=1dummy→2→1→3→4→5
第二组反转(i=0/1)反转后 pre=4, cur=53←4 5(临时断裂)
第二组拼接nxt=3, 3→5, 1→4, p0=3dummy→2→1→4→3→5
循环结束n=1 < 2,剩余节点 5 不处理dummy→2→1→4→3→5
返回结果dummy.next=22→1→4→3→5

六、复习关键避坑点

  1. 拼接时的指针顺序:必须先保存 p0.nextnxt),再修改 p0.next.next,最后修改 p0.next,否则会丢失节点引用;
  2. p0 的更新时机:只有拼接完成后,才能把 p0 设为当前组的组尾(nxt),否则会导致下一组前驱节点错误;
  3. 剩余节点处理:通过 n >= k 控制循环,确保最后不足 k 个的节点不反转;
  4. 虚拟头节点的必要性:如果没有 dummy,第一组反转后需要单独修改 head,逻辑会更复杂。

七、总结(复习速记)

  1. 核心逻辑统计长度→分组反转→拼接边界,其中「拼接边界」是核心难点;
  2. 关键指针
    • p0:组前驱,负责拼接反转后的组;
    • pre/cur:组内反转的双指针;
    • dummy:统一头节点处理逻辑;
  3. 执行顺序:先反转组内节点,再拼接组的首尾,最后更新 p0 处理下一组。

Read more

从零开始:在本地搭建一个带知识库的 AI 助手(Ollama + Open WebUI)

从零开始:在本地搭建一个带知识库的 AI 助手(Ollama + Open WebUI)

一文讲清楚:要选哪些工具、需要什么环境、整体架构长什么样,以及一步步实现到能用的程度。 一、为什么要在本地搭一个 AI 助手? 过去一年,大模型从“新奇玩意儿”迅速变成“日常生产力工具”。但如果你只用网页版 ChatGPT / 文心一言 / 通义千问,会碰到几个很现实的问题: * 数据隐私:公司内部文档、个人笔记、聊天记录,你敢全部塞到线上吗? * 网络依赖:在飞机上、高铁里,或者公司内网严格管控时,在线 AI 直接“失联”。 * 额度与费用:免费额度有限,稍微重度一点就要付费,而且你也不知道自己的数据会不会被拿去训练。 本地部署一套 “AI + 知识库” 的好处就非常直观: 1. 数据完全不出本地,满足隐私合规要求。 2. 断网也能用,随时随地调取你的“第二大脑”。 3. 可定制:可以给团队搭一个“

By Ne0inhk
Flutter for OpenHarmony: Flutter 三方库 sanitize_html 彻底杜绝 XSS 注入风险(鸿蒙 Web 内容安全净化)

Flutter for OpenHarmony: Flutter 三方库 sanitize_html 彻底杜绝 XSS 注入风险(鸿蒙 Web 内容安全净化)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在开发 OpenHarmony 应用时,如果我们需要在 UI 中渲染来自后端的 HTML 内容(例如文章正文、用户评论),或者使用 flutter_html 等库,一个致命的安全风险就是 XSS (跨站脚本攻击)。恶意代码可能会通过 <script> 标签或 onerror 属性在你的 App 内执行非法逻辑。 sanitize_html 是一个轻量级且极高效的 HTML 净化库。它采用白名单机制,能瞬间过滤掉所有不安全的标签和属性,确保你在鸿蒙 App 内渲染的每一行 Web 内容都是绝对安全的。 一、核心防御机制解析 sanitize_html 遵循“默认拒绝”

By Ne0inhk

前端保持和服务器时间同步的方法【使用vue3举例】

你只管努力!剩下的交给时间! 目录 * 引言: * 方法一: 轮询(定时请求服务器时间) * 优点: * 缺点: * 方法二:使用WebSocket * 优点: * 缺点: * 方法三:时间戳校正 * 优点: * 缺点: * 方法四: 使用NTP(网络时间协议) * 优点: * 缺点: * 方法五:使用SSE(Server-Sent Events) * 优点: * 缺点: * 总结: 引言: 保持前端与服务器时间同步是一个常见的需求,特别是在需要确保时间一致性的应用中,比如在线投票、实时聊天或游戏等。以下是一些方法来实现这一目标: 方法一: 轮询(定时请求服务器时间) 可以定时向服务器发送请求获取当前时间,以此来更新前端的时间显示。 <template><div><h1>当前时间:

By Ne0inhk
前端异常捕获与统一格式化:从 console.log(error) 到服务端上报

前端异常捕获与统一格式化:从 console.log(error) 到服务端上报

🧑 博主简介:ZEEKLOG博客专家,「历代文学网」(公益文学网,PC端可以访问:https://lidaiwenxue.com/#/?__c=1000,移动端可关注公众号 “ 心海云图 ” 微信小程序搜索“历代文学”)总架构师,首席架构师,也是联合创始人!16年工作经验,精通Java编程,高并发设计,分布式系统架构设计,Springboot和微服务,熟悉Linux,ESXI虚拟化以及云原生Docker和K8s,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。 🤝商务合作:请搜索或扫码关注微信公众号 “ 心海云图 ” 前端异常捕获与统一格式化:从 console.log(error) 到服务端上报 引言 在前端开发中,异常监控是保证应用稳定性的重要一环。当用户遇到页面白屏、功能不可用等问题时,如果能及时收集到详细的错误信息(包括堆栈、

By Ne0inhk