25. K 个一组翻转链表

25. K 个一组翻转链表

困难

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

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例 1:

复制代码
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]

示例 2:

复制代码
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]

提示:

  • 链表中的节点数目为 n
  • 1 <= k <= n <= 5000
  • 0 <= Node.val <= 1000

进阶: 你可以设计一个只用 O(1) 额外内存空间的算法解决此问题吗?

📝 核心笔记:K 个一组翻转链表 (Reverse Nodes in k-Group)

1. 核心思想 (一句话总结)

"先数数,够数就翻,翻完缝合。" 这道题本质上是 LeetCode 206 (反转链表)LeetCode 92 (反转链表 II) 的结合体。 我们需要维护一个"哨兵" p0,它始终站在 "当前要翻转的这一组" 的前一个位置。

  • 组内:标准的链表翻转(局部倒序)。
  • 组间:翻转后,原来的头变尾,原来的尾变头,需要把它们和前后段重新接上(缝合)。
2. 算法流程 (三步走)
  1. 统计长度 (Count)
    • 先遍历一遍链表,得到总长度 n
    • 作用:决定循环次数。只有当剩余节点数 n >= k 时才翻转,不够 k 个直接保留不动。
  1. 组内翻转 (Reverse Inner)
    • 循环 k 次,执行标准的"反转链表"逻辑。
    • 翻转结束后,pre 变成了当前组的新头,cur 变成了下一组的开头。
  1. 缝合接口 (Connect)
    • 这是最难的一步。我们需要三个节点参与缝合:
      • p0:当前组的前驱(哨兵)。
      • p0.next:当前组 原来的头 (翻转后变成了 现在的尾)。
      • cur:下一组的头。
      • pre:当前组 现在的头
    • 连接逻辑
      1. p0.next.next = cur:让本组现在的尾巴,连上下一组的头。
      2. p0.next = pre:让哨兵连上本组现在的头。
      3. p0 = ...:哨兵走到本组现在的尾巴位置,准备下一轮。
🔍 代码回忆清单
复制代码
// 题目:LC 25. Reverse Nodes in k-Group
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        // 1. 统计节点总数
        int n = 0;
        for (ListNode cur = head; cur != null; cur = cur.next) {
            n++;
        }

        ListNode dummy = new ListNode(0, head);
        ListNode p0 = dummy; // p0 是每一组的前驱节点 (哨兵)
        
        // cur 和 pre 放在外面初始化,因为它们在循环之间是传递的
        ListNode pre = null;
        ListNode cur = head;

        // 2. 循环处理每一组
        // 只要剩余节点数 n >= k,就进行翻转
        for (; n >= k; n -= k) {
            
            // 3. 组内翻转 (标准的 LC 206 逻辑)
            // 翻转 k 个节点
            for (int i = 0; i < k; i++) { 
                ListNode nxt = cur.next;
                cur.next = pre; 
                pre = cur;
                cur = nxt;
            }
            // 循环结束后:
            // pre 是当前组的新头
            // cur 是下一组的头 (未处理的第一个)
            
            // 4. 缝合 (核心难点)
            // p0.next 是这一组"原来的头",翻转后它变成了"现在的尾"
            ListNode nxt = p0.next; // 暂存一下"现在的尾"
            
            // 现在的尾 -> 指向下一组的头
            // (对应图解:1 连向 3)
            p0.next.next = cur; 
            
            // 哨兵 -> 指向当前组的新头
            // (对应图解:dummy 连向 2)
            p0.next = pre; 
            
            // 5. 更新 p0
            // p0 走到当前组的尾部,作为下一组的哨兵
            p0 = nxt; 
        }
        
        return dummy.next;
    }
}
⚡ 快速复习 CheckList (易错点)
  • \] **为什么要有** **n****?**

    • 如果不统计 n,你很难判断最后剩下的节点是否足 k 个。如果不足 k 个却翻转了,就不符合题意了。
    • n >= k 是循环继续的硬性条件。
  • \] **缝合顺序乱了?**

    • 口诀:先连尾巴,再连头。
    • 先让 旧头(新尾) 连上 cur (下一段)。
    • 再让 哨兵 连上 pre (新头)。
  • \] **p0 怎么移动?**

    • p0 必须移动到当前子链表的末尾。
    • 也就是翻转前的 head,翻转后的 tail。代码里巧妙地利用 p0.next(在修改前)获取了这个节点。
🖼️ 数字演练

链表:1 -> 2 -> 3 -> 4 -> 5k = 2``n = 5, dummy -> 1. p0 = dummy.

Round 1 (处理 1, 2):

  • Inner Loop:
    • 翻转后:2 -> 1
    • pre 指向 2,cur 指向 3。
  • Wiring (缝合):
    • p0.next 是 1 (旧头/新尾)。
    • 1.next = 3 (连上下一个组)。
    • dummy.next = 2 (哨兵连新头)。
    • 链表变为:dummy -> 2 -> 1 -> 3 -> 4 -> 5.
  • Update : p0 移动到 1。

Round 2 (处理 3, 4):

  • Inner Loop:
    • 翻转后:4 -> 3
    • pre 指向 4,cur 指向 5。
  • Wiring:
    • p0.next 是 3。
    • 3.next = 5.
    • 1.next = 4.
    • 链表变为:dummy -> 2 -> 1 -> 4 -> 3 -> 5.
  • Update : p0 移动到 3。

End : n=11 < 2,循环结束。剩下的 5 不动。

相关推荐
忧郁的Mr.Li10 分钟前
SpringBoot中实现多数据源配置
java·spring boot·后端
啊森要自信11 分钟前
CANN ops-cv:AI 硬件端视觉算法推理训练的算子性能调优与实战应用详解
人工智能·算法·cann
yq19820430115623 分钟前
静思书屋:基于Java Web技术栈构建高性能图书信息平台实践
java·开发语言·前端
一个public的class24 分钟前
你在浏览器输入一个网址,到底发生了什么?
java·开发语言·javascript
有位神秘人25 分钟前
kotlin与Java中的单例模式总结
java·单例模式·kotlin
golang学习记30 分钟前
IntelliJ IDEA 2025.3 重磅发布:K2 模式全面接管 Kotlin —— 告别 K1,性能飙升 40%!
java·kotlin·intellij-idea
爬山算法33 分钟前
Hibernate(89)如何在压力测试中使用Hibernate?
java·压力测试·hibernate
仟濹34 分钟前
算法打卡day2 (2026-02-07 周五) | 算法: DFS | 3_卡码网99_计数孤岛_DFS
算法·深度优先
驭渊的小故事37 分钟前
简单模板笔记
数据结构·笔记·算法
YuTaoShao1 小时前
【LeetCode 每日一题】1653. 使字符串平衡的最少删除次数——(解法一)前后缀分解
算法·leetcode·职场和发展