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

【LeetCode 热题 100】25. K 个一组翻转链表——迭代+哨兵

Problem: 25. K 个一组翻转链表
题目:给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

文章目录

  • 整体思路
  • 完整代码
  • 时空复杂度
    • 时间复杂度:O(N)
    • 空间复杂度:O(1)

整体思路

这段代码旨在解决一个高难度的经典链表问题:K 个一组翻转链表 (Reverse Nodes in k-Group)。问题要求将一个链表中的节点每 k 个一组进行翻转,如果最后剩余的节点数不足 k,则保持原样。

该算法采用了一种精巧的 迭代 方法,通过分组处理和局部反转来完成任务。它结合了 哨兵节点 和多个指针,以清晰地管理复杂的链接关系。

其核心逻辑步骤如下:

  1. 预计算链表长度

    • 算法首先通过一次遍历计算出链表的总长度 n
    • 目的:这使得在主循环中可以方便地判断剩余的节点是否还足够形成一个大小为 k 的组,即 while (n >= k)
  2. 初始化

    • 哨兵节点:创建一个 dummy 节点,其 next 指向原始头节点 head。这极大地简化了对链表头部的修改,因为第一组的反转与其他组的逻辑可以完全统一。
    • 关键指针
      • groupPrev: 指向当前待反转组的前一个节点。它负责将处理好的前一部分链表与当前反转好的组连接起来。初始时指向 dummy
      • pre: 用于在组内进行反转的指针,初始为 null
      • cur: 指向在组内当前正在处理的节点,初始为 head
  3. 分组迭代与反转

    • 算法进入一个主 while 循环,只要剩余节点数 n 大于等于 k,就处理一个组。
    • 组内反转
      • 在主循环内部,一个 for 循环执行 k 次,用于反转当前组的 k 个节点。
      • 这部分使用了标准的迭代反转链表的技巧:通过 precur 指针,逐个将 cur 节点的 next 指向前一个节点 pre,从而实现局部反转。
      • for 循环结束后,pre 指向了该组反转后的新头节点,而 cur 指向了下一组的起始节点。
    • 重新链接
      • 这是算法最精妙的部分。反转一个组后,需要将其正确地接回主链表中。
      • preTail = groupPrev.next: 首先,找到反转前该组的头节点,这个节点在反转后会成为尾节点。我们将其保存为 preTail
      • preTail.next = cur: 将反转后的组的尾部 (preTail) 连接到下一组的头部 (cur)。
      • groupPrev.next = pre: 将前一个组的尾部 (groupPrev) 连接到当前反转后组的新头部 (pre)。
      • groupPrev = preTail: 为下一轮循环做准备,将 groupPrev 指针移动到刚刚处理完的组的尾节点上。
  4. 返回结果

    • 循环结束后,所有满足条件的组都已被反转并正确链接。dummy.next 指向了整个新链表的头节点,将其返回。

完整代码

/*** Definition for singly-linked list.*/
class ListNode {int val;ListNode next;ListNode() {}ListNode(int val) { this.val = val; }ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}class Solution {/*** K 个一组翻转链表。* @param head 原始链表的头节点* @param k 每组的节点数* @return 翻转后链表的头节点*/public ListNode reverseKGroup(ListNode head, int k) {// 步骤 1: 预计算链表的总长度 nint n = 0;ListNode cur = head;while (null != cur) {n++;cur = cur.next;}// 创建哨兵节点,简化头部操作ListNode dummy = new ListNode(0, head);// groupPrev 指向当前待反转组的前一个节点ListNode groupPrev = dummy;// pre 用于组内反转,将成为反转后组的新头ListNode pre = null;// cur 指向组内当前正在处理的节点cur = head;// 主循环:只要剩余节点数足够 k 个,就处理一个组while (n >= k) {// 更新剩余节点数n -= k;// 步骤 2: 核心反转逻辑,反转 k 个节点for (int i = 0; i < k; i++) {ListNode nxt = cur.next; // 暂存下一个节点cur.next = pre;          // 当前节点指向前一个节点pre = cur;               // pre 前移cur = nxt;               // cur 前移}// 步骤 3: 重新链接反转后的组// preTail 是反转前该组的头节点,反转后它变成了尾节点ListNode preTail = groupPrev.next;// 将反转后组的尾部 (preTail) 连接到下一组的头部 (cur)preTail.next = cur;// 将前一个组的尾部 (groupPrev) 连接到反转后组的新头部 (pre)groupPrev.next = pre;// 为下一次循环做准备:更新 groupPrev,它现在是刚刚处理完的组的尾节点groupPrev = preTail;}return dummy.next;}
}

时空复杂度

时间复杂度:O(N)

  1. 计算长度:第一个 while 循环遍历整个链表一次,时间复杂度为 O(N)
  2. 分组反转:主 while 循环和内部的 for 循环结合起来,确保了每个节点只会被访问和操作常数次。while 循环执行 N/k 次,for 循环执行 k 次,总操作数与 (N/k) * k = N 成正比。因此,这部分的时间复杂度也是 O(N)

综合分析
算法的总时间复杂度是 O(N) + O(N) = O(N)。它对链表进行了两次线性扫描(一次计数,一次反转)。因此,最终时间复杂度为 O(N)

空间复杂度:O(1)

  1. 主要存储开销:算法只使用了 dummy, groupPrev, pre, cur, nxt, preTail 等固定数量的指针变量。
  2. 空间大小:这些变量的数量不随输入链表的长度 N 或分组大小 k 的变化而改变。

综合分析
算法没有使用任何与输入规模成比例的额外数据结构。因此,其额外辅助空间复杂度为 O(1)。这是一个高效的原地算法。

参考灵神

http://www.dtcms.com/a/276160.html

相关文章:

  • Spring AOP 是如何生效的(入口源码级解析)?
  • SpringBootloggers未授权访问漏洞处理
  • Flink创建执行环境的三种方式,也是Flink搭建程序的第一步
  • [特殊字符] 实时数据洪流突围战:Flink+Paimon实现毫秒级分析的架构革命(附压测报告)——日均百亿级数据处理成本降低60%的工业级方案
  • Java应用全链路故障排查实战指南:从系统资源到JVM深度诊断
  • WebSocket:构建实时交互的 Web 应用
  • C# VB.NET多进程-管道通信,命名管道(Named Pipes)
  • C语言结构体
  • C++---<cctype>
  • 2025科大讯飞AI大赛<大模型技术方向>(Datawhale AI 夏令营)
  • 解决bash终端的路径名称乱码问题
  • Redis渗透思路总结
  • Theo Mandel的用户界面设计三大黄金规则
  • 新手向:使用Python自动化清理指定目录中的临时文件
  • 脉冲神经网络膜电位泄漏系数学习:开启时空动态特征提取的新篇章
  • 实验一 接苹果
  • 配置驱动开发:初探零代码构建嵌入式软件配置工具
  • Windows 用户账户控制(UAC)绕过漏洞
  • 时序分解 | Matlab基于GWO-FMD基于灰狼算法优化特征模态分解-2025-7-12
  • 实现Service和UI通信,并且能够手动回收的解决方案——共享ViewModel
  • 卫星通信终端天线的5种对星模式之二:DVB跟踪
  • C++类模板继承部分知识及测试代码
  • Final
  • 剑指offer——树:二叉树的深度
  • 【C++小白逆袭】内存管理从崩溃到精通的秘籍
  • JVM 中“对象存活判定方法”全面解析
  • JVM的垃圾回收算法和多种GC算法
  • Git 相关的常见面试题及参考答案
  • 人工智能安全基础复习用:可解释性
  • 通过渐进蒸馏实现扩散模型的快速采样