LeetCode 25:K 个一组翻转链表(指针地狱?其实有套路)

59 阅读3分钟

在链表题里,如果说有一道题能真正检验你对「指针」的理解深度,那 K 个一组翻转链表一定榜上有名。

很多人第一次看这题的感受是:

  • 思路好像懂了
  • 代码能抄出来
  • 但指针一动,脑子就乱

这篇笔记,我想做一件事:
把这道题拆到你能“在脑子里完整跑一遍指针”为止。


一、题目要求回顾

给你一个链表,每 k 个节点一组进行翻转

  • k 个一组必须完整,才能翻转
  • 如果最后剩下的节点不足 k 个,保持原顺序
  • 只能使用 O(1) 额外空间

示例:

输入:1 -> 2 -> 3 -> 4 -> 5, k = 2
输出:2 -> 1 -> 4 -> 3 -> 5

二、这道题的本质是什么?

一句话概括:

在链表中,反复对固定长度为 k 的区间做局部翻转

所以问题被拆成了三件事:

  1. 如何判断后面是否还有 k 个节点?
  2. 如何只翻转 [start, end] 这一段?
  3. 翻转后,如何把链表重新接回去?

三、为什么一定要用 dummy 节点?

这是链表高质量解法的标配。

ListNode dummy = new ListNode(0);
dummy.next = head;

原因只有一个:

  • 第一组翻转时,头节点会发生变化
  • 使用 dummy,可以 把“头节点”当成普通节点处理
  • 所有翻转逻辑完全一致,不需要特殊判断

四、核心思路总览

整体代码是一个「按组处理」的循环:

while (true) {
    1. 判断是否够 k 个
    2. 翻转当前这 k 个
    3. 移动 pre,进入下一组
}

关键指针只有三个:

  • pre:当前要翻转这一组的前驱节点
  • kth:当前组的第 k 个节点
  • nextGroupHead:下一组的起点

五、完整代码

class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode pre = dummy;

        while (true) {
            // 1. 找到第 k 个节点
            ListNode kth = pre;
            for (int i = 0; i < k; i++) {
                kth = kth.next;
                if (kth == null) {
                    return dummy.next;
                }
            }

            // 2. 记录下一组的起点
            ListNode nextGroupHead = kth.next;

            // 3. 翻转当前这 k 个节点
            ListNode prev = nextGroupHead;
            ListNode curr = pre.next;

            while (curr != nextGroupHead) {
                ListNode temp = curr.next;
                curr.next = prev;
                prev = curr;
                curr = temp;
            }

            // 4. 重新接回链表
            ListNode newHead = pre.next;
            pre.next = kth;
            pre = newHead;
        }
    }
}

六、如何判断“是否够 k 个”?

这一段是只检查,不修改链表

ListNode kth = pre;
for (int i = 0; i < k; i++) {
    kth = kth.next;
    if (kth == null) {
        return dummy.next;
    }
}

含义很明确:

  • pre 出发向后走 k 步
  • 中途遇到 null,说明不足 k 个
  • 不翻转,直接返回结果

七、局部翻转的精髓:为什么 prev 要指向 nextGroupHead?

ListNode prev = nextGroupHead;
ListNode curr = pre.next;

这一点非常关键。

我们翻转的是:

[pre.next ... kth]

但让 prev 一开始指向 nextGroupHead,意味着:

  • 翻转完成后,尾节点会自动接到下一组
  • 不需要额外处理尾指针
  • 翻转逻辑和「反转整个链表」完全一致

这是一个非常高级但非常稳的技巧


八、最容易让人懵的三行代码

ListNode newHead = pre.next;
pre.next = kth;
pre = newHead;

逐行解释:

newHead = pre.next
翻转前的头节点,翻转后会变成 这一组的尾节点

pre.next = kth
把前驱节点接到翻转后的新头

pre = newHead
让 pre 移动到这一组的尾部,为下一轮做准备

关键理解一句话:

pre 是“游标”,不是 dummy 的别名


九、指针变化示意

k = 2 为例:

翻转前:

dummy -> 1 -> 2 -> 3 -> 4

翻转第一组后:

dummy -> 2 -> 1 -> 3 -> 4
              ↑
             pre

第二轮自然处理 [3, 4],逻辑完全复用。


十、复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

每个节点只被访问和翻转一次。


十一、小总结

这道题真正考察的不是“会不会翻转链表”,而是:

  • 是否理解 指针在链表中的相对关系
  • 是否能把“区间翻转”抽象成通用模板
  • 是否能用 dummy + pre 把复杂情况统一掉

如果你能把这道题讲清楚,
80% 的链表题你都会发现:原来它们是一个套路。