K 个一组翻转链表
在 LeetCode 上查看 ↗语音讲解
开车或通勤时可听,跟着思路走一遍
题目描述
给你链表的头节点 head,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例 1
示例 2
示例 3
模拟答题者思考
1. 我先想暴力:把节点值复制到数组,按每 k 个一组反转数组片段再重建链表——能过,但题目要求「实际进行节点交换」,且多用了 O(n) 额外空间。
2. 重复在哪里?#24 两两交换是 k=2 的特例;核心仍是「确定一组边界 → 局部反转 → 把反转后的组接回主链 → 前驱移到本组尾节点」。
3. 难点是边界:剩余节点不足 k 个时不翻转。因此每轮先从 group_prev 走 k 步找 kth;找不到就直接结束。
4. 找到 kth 后,在 [group_prev.next, kth] 闭区间内做标准链表反转,反转边界是 group_next = kth.next。反转完把 group_prev.next 指向新头 kth,再令 group_prev = 原组头 处理下一组。
5. 第一组翻转会改变头节点——加 dummy 哨兵后,group_prev 从 dummy 出发,与 #24、合并链表等题同一套路;整体时间 O(n),每个节点最多被访问常数次。
变量语义(先读这三句再编码)
| 变量 | 类型 | 语义(三句法) |
|---|---|---|
dummy | ListNode* | 定义:哨兵头节点,dummy.next = head维护:始终位于真实头节点之前,统一处理「第一组翻转后新头节点」的边界 更新:创建后不再移动,最终返回 dummy.next |
group_prev | ListNode* | 定义:当前待翻转 k 组的前驱指针维护:每组翻转完成后, group_prev 应停在「刚翻转完的那一组的尾节点」(即翻转前的组头)更新:翻转一组后 group_prev = group_start(原组头变尾),下一轮从 group_prev.next 继续 |
kth | ListNode* | 定义:从 group_prev 出发向后走 k 步得到的节点,即当前组的尾节点维护:若 kth 为 null,说明剩余不足 k 个节点,整题结束更新:每轮用 getKth(group_prev, k) 重新计算 |
group_next | ListNode* | 定义:当前组尾节点 kth 的下一个节点,即下一组的起点维护:翻转时作为内层反转循环的终止边界( curr != group_next)更新:每轮在确认 kth 存在后令 group_next = kth.next |
prev / curr | ListNode* | 定义:组内局部反转的双指针,prev 初始为 group_next,curr 初始为 group_prev.next维护:标准单链表反转:保存 tmp = curr.next,curr.next = prev,前移 prev 与 curr更新:当 curr == group_next 时本组反转完成;此时 kth 成为新组头,group_start 成为新组尾 |
落码步骤
1. 创建哨兵 dummy = ListNode(0, head),group_prev = dummy
2. 循环:调用 getKth(group_prev, k) 找当前组尾 kth;若为 null 则剩余不足 k 个,跳出循环
3. 记录 group_next = kth.next,group_start = group_prev.next(翻转后将变成本组尾)
4. 在 [group_start, kth] 内局部反转:prev = group_next,curr = group_start,标准三指针翻转直到 curr == group_next
5. 接回主链:group_prev.next = kth(新组头),group_prev = group_start(新组尾作下轮前驱);返回 dummy.next
代码实现
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
dummy = ListNode(0, head) # 哨兵,统一处理头节点变化
group_prev = dummy
while True:
kth = self.getKth(group_prev, k)
if not kth:
break
group_next = kth.next
group_start = group_prev.next
# 在 [group_start, kth] 内局部反转,边界为 group_next
prev, curr = group_next, group_start
while curr != group_next:
tmp = curr.next
curr.next = prev
prev = curr
curr = tmp
group_prev.next = kth # 前驱接到翻转后的新头
group_prev = group_start # 原组头变尾,作为下一组前驱
return dummy.next
def getKth(self, curr: ListNode, k: int) -> Optional[ListNode]:
while curr and k > 0:
curr = curr.next
k -= 1
return curr
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode dummy(0, head); // 哨兵,统一处理头节点变化
ListNode* group_prev = &dummy;
while (true) {
ListNode* kth = getKth(group_prev, k);
if (!kth) break;
ListNode* group_next = kth->next;
ListNode* group_start = group_prev->next;
// 在 [group_start, kth] 内局部反转,边界为 group_next
ListNode* prev = group_next;
ListNode* curr = group_start;
while (curr != group_next) {
ListNode* tmp = curr->next;
curr->next = prev;
prev = curr;
curr = tmp;
}
group_prev->next = kth; // 前驱接到翻转后的新头
group_prev = group_start; // 原组头变尾,作为下一组前驱
}
return dummy.next;
}
private:
ListNode* getKth(ListNode* curr, int k) {
while (curr && k > 0) {
curr = curr->next;
k--;
}
return curr;
}
};
// 时间 O(n),空间 O(1)
复杂度分析
O(n)
O(1)
常见坑
忘记先检查剩余是否足 k 个:不足时应保持原顺序直接结束,不能强行翻转。
局部反转边界写错:prev 应初始化为 group_next(不是 null),否则翻转后无法与后续链表正确衔接。
翻转后忘记更新 group_prev:应移到原组头(现组尾),否则下一轮会从已翻转区域重复操作或断链。
必测边界 Case
head = [1,2,3], k = 1 → [1,2,3](每组 1 个,等价于不翻转)
head = [1,2,3,4], k = 2 → [2,1,4,3](全部参与翻转)
head = [1,2,3,4,5], k = 3 → [3,2,1,4,5](最后 4、5 保持原序)