如何 K 个一组反转链表?
简化版
K 个一组反转链表的核心是先确认后面够不够 K 个节点,够就反转这一段,不够就保持原样。通常用 dummy 维护总头,用 groupPrev 指向当前组前驱,反转后把前驱、组尾和下一组接回去。
详细版
流程可以拆成 4 步:找到当前组第 K 个节点 kth;记录下一组头 groupNext = kth.next;反转 [groupPrev.next, groupNext) 这段;把 groupPrev.next 接到反转后的新头,把旧组头接到 groupNext,再移动 groupPrev 到旧组头。
dummy -> 1 -> 2 -> 3 -> 4 -> 5, k=2
after: dummy -> 2 -> 1 -> 4 -> 3 -> 5
这题难点不在反转本身,而在边界:不足 K 个不能反转,反转后组与组之间要接好。
完整版教学
一、K 组反转比普通反转多了分组边界
普通反转只需要把整条链表翻过来。K 个一组反转则要求每 K 个节点反转一次,最后不足 K 个保持原顺序。
input: 1 -> 2 -> 3 -> 4 -> 5
k = 2
output: 2 -> 1 -> 4 -> 3 -> 5
这里节点 5 不足一组,所以不动。这个“不足 K 个不反转”的条件是最容易漏的边界。
记忆钩子:先数够 K 个,再动手反转;没数够就收工。
二、用 dummy 和 groupPrev 稳住连接点
反转第一组会改变头节点,所以需要 dummy。groupPrev 表示当前组的前驱。
dummy -> 1 -> 2 -> 3 -> 4 -> 5
^
groupPrev
每轮要做的是反转 groupPrev.next 开始的 K 个节点。反转后,groupPrev.next 要指向这一组的新头。
如果没有 groupPrev,你会很难把上一组和当前组接起来。
三、先找到 kth 和 groupNext
从 groupPrev 出发往后走 K 步,找到当前组第 K 个节点。
groupPrev -> 1 -> 2 -> 3
k=2, kth=2, groupNext=3
如果走不到 K 步,说明剩余节点不足一组,直接返回 dummy.next。
ListNode kth = groupPrev;
for (int i = 0; i < k && kth != null; i++) {
kth = kth.next;
}
if (kth == null) break;
ListNode groupNext = kth.next;
groupNext 必须提前保存,否则反转过程中 kth.next 会被改掉。
四、反转半开区间 [start, groupNext)
当前组起点是 groupPrev.next,终点是不包含的 groupNext。可以用半开区间思维:
start = groupPrev.next
reverse until cur == groupNext
代码:
ListNode prev = groupNext;
ListNode cur = groupPrev.next;
while (cur != groupNext) {
ListNode next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
为什么 prev 初始化为 groupNext?因为反转后旧组头会变成组尾,它的 next 应该接到下一组头。
五、反转后重新接回总链表
反转前的组头会变成反转后的组尾,要先保存。
ListNode oldHead = groupPrev.next;
// reverse ...
groupPrev.next = kth;
groupPrev = oldHead;
以 1 -> 2 -> 3 -> 4、k=2 为例:
before group:
groupPrev -> 1 -> 2 -> groupNext(3)
after reverse:
groupPrev -> 2 -> 1 -> 3
^
next groupPrev
然后下一轮从旧组头,也就是新组尾继续。
六、复杂度和易错点
每个节点最多被访问和反转常数次,所以时间 O(n)。只用几个指针变量,额外空间 O(1)。
容易错的点有 4 个:
1. 不足 K 个却反转了
2. 没保存 groupNext
3. 反转后 groupPrev.next 接错
4. groupPrev 没移动到旧组头
写这题时建议先画一组 k=2,再画一组 k=3。只在脑子里转指针,很容易绕进去。
七、常见误区与追问
- 误区:剩多少节点都要反转。 题目通常要求不足 K 个保持原样,要先确认够 K 个。
- 误区:反转后 kth 还是原来的尾节点。 在当前组反转后,原 kth 会成为新头。
- 误区:不用保存 groupNext 也能接回。 反转会改指针,不提前保存很容易丢失下一段。
- 追问:为什么 prev 初始为 groupNext? 这样旧组头反转后自然指向下一组,少一次额外接线。
- 追问:复杂度是多少? 时间 O(n),额外空间 O(1),每个节点只处理常数次。
- 追问:k=1 怎么办? 每组一个节点,链表不变,代码也应自然返回原链表。
八、加强记忆
K 个一组反转记成“数 K、断边界、反半开、接回去”。先从 groupPrev 找到 kth,不够 K 个就停;保存 groupNext,反转 [groupPrev.next, groupNext);反转后 groupPrev.next 指向新头,groupPrev 移到旧组头。dummy 稳住总头,半开区间稳住边界。