LeetCode 25:K 个一组翻转链表
LeetCode 25:K 个一组翻转链表

问题定义与核心挑战
给定链表头节点 head 和整数 k,要求 每 k 个节点一组翻转链表,不足 k 个的节点保持原序。例如:
- 输入:
head = [1,2,3,4,5], k=2→ 输出:[2,1,4,3,5] - 输入:
head = [1,2,3,4,5], k=3→ 输出:[3,2,1,4,5]
核心挑战:
- 分组与边界判断:如何确定每组的范围(找到尾节点),并处理不足
k个节点的情况。 - 链表反转:如何高效反转每组内的节点,并连接前后部分。
- 递归与迭代的平衡:递归思路更直观,但需理解栈的调用逻辑。
核心思路:递归 + 局部反转
- 分组定位:找到当前组的尾节点(从
head出发走k-1步),若不足k个节点则直接返回。 - 局部反转:反转当前组内的节点(从
head到尾节点)。 - 递归连接:将反转后的组与剩余链表(尾节点的下一个节点开始)递归处理的结果连接。
算法步骤详解
步骤 1:定位当前组的尾节点
- 从
head出发,移动k-1次,找到当前组的尾节点tail。 - 若移动过程中
tail变为null,说明剩余节点不足k个,直接返回head(无需翻转)。
ListNode tail = head;for (int i = 0; i < k - 1; i++) { if (tail == null) return head; // 不足k个,直接返回 tail = tail.next;}if (tail == null) return head; // 双重保险(罕见情况)
步骤 2:保存剩余链表并反转当前组
- 剩余链表的头节点为
tail.next(记为rest)。 - 反转当前组(
head到tail),得到新的头节点newHead(原tail)。
ListNode rest = tail.next; // 剩余链表的头ListNode newHead = reverse(head, tail); // 反转当前组
步骤 3:递归处理剩余链表并连接
- 递归处理剩余链表
rest,得到处理后的头节点restHead。 - 将当前组的尾节点(原
head)连接到restHead。
ListNode restHead = reverseKGroup(rest, k); // 递归处理剩余部分head.next = restHead; // 连接反转后的尾(原head)与剩余部分return newHead; // 返回当前组反转后的头(原tail)
步骤 4:实现局部反转函数 reverse
反转从 head 到 tail 的链表,返回新的头节点(原 tail):
- 用三指针法(
prev,curr,next)逐步反转节点。 - 最后处理尾节点
tail,使其指向反转后的前一个节点。
private ListNode reverse(ListNode head, ListNode tail) { ListNode prev = null; ListNode curr = head; while (curr != tail) { // 遍历到尾节点前一个 ListNode next = curr.next; curr.next = prev; // 反转指针 prev = curr; curr = next; } curr.next = prev; // 处理尾节点 return curr; // 新头是原tail}
完整代码(Java)
/** * Definition for singly-linked list. * public 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 { public ListNode reverseKGroup(ListNode head, int k) { // 边界处理:空链表或k=1(无需翻转) if (head == null || k == 1) { return head; } // 步骤1:定位当前组的尾节点 ListNode tail = head; for (int i = 0; i < k - 1; i++) { if (tail == null) { // 不足k个节点,直接返回 return head; } tail = tail.next; } if (tail == null) { // 双重检查(理论上不会触发) return head; } // 步骤2:保存剩余链表,反转当前组 ListNode rest = tail.next; // 剩余链表的头 ListNode newHead = reverse(head, tail); // 反转当前组(head->tail) // 步骤3:递归处理剩余链表,连接结果 ListNode restHead = reverseKGroup(rest, k); // 递归处理剩余部分 head.next = restHead; // 原head现在是反转后的尾,连接剩余部分 return newHead; // 返回当前组反转后的头(原tail) } // 辅助函数:反转从head到tail的链表,返回新的头(原tail) private ListNode reverse(ListNode head, ListNode tail) { ListNode prev = null; ListNode curr = head; while (curr != tail) { // 遍历到尾节点前一个 ListNode next = curr.next; curr.next = prev; // 反转指针 prev = curr; curr = next; } curr.next = prev; // 处理尾节点,使其指向反转后的前一个节点 return curr; // 新头是原tail }}
关键逻辑解析
1. 分组定位的细节
- 循环
k-1次找尾节点:确保当前组恰好有k个节点(head到tail共k个)。 - 若中途
tail为null,说明剩余节点不足k个,直接返回(避免无效反转)。
2. 局部反转的三指针法
prev初始为null,curr从head开始,逐步将curr.next指向prev,实现反转。- 循环结束时,
curr指向tail,需单独处理tail的next(指向prev,即反转后的前一个节点)。
3. 递归的作用
- 递归处理剩余链表
rest,天然实现“分组处理”的逻辑,代码更简洁。 - 递归栈的深度为
O(n/k)(每组递归一次),空间复杂度可接受(n为链表长度)。
4. 链表连接的关键
- 反转后,原
head变为当前组的尾节点,需将其next指向剩余链表的处理结果(restHead)。
示例验证(以示例 1 为例)
输入:head = [1,2,3,4,5], k=2
-
第一次递归:
- 定位
tail = 2(head=1移动 1 次),rest = 3。 - 反转
1->2得到2->1,newHead=2。 - 递归处理
rest=3,进入第二次递归。
- 定位
-
第二次递归:
- 定位
tail = 4(head=3移动 1 次),rest = 5。 - 反转
3->4得到4->3,newHead=4。 - 递归处理
rest=5,进入第三次递归。
- 定位
-
第三次递归:
- 定位
tail时,head=5移动 1 次后tail=null,返回5。 restHead=5,原head=3(反转后的尾)的next=5→3->5。- 返回
4->3->5。
- 定位
-
回溯连接:
- 第二次递归中,原
head=3的next=4->3->5→1->4->3->5。 - 第一次递归中,原
head=1的next=4->3->5→2->1->4->3->5。
- 第二次递归中,原
复杂度分析
- 时间复杂度:
O(n)。每个节点被访问两次(一次定位尾节点,一次反转)。 - 空间复杂度:
O(n/k)。递归栈的深度为分组数(最坏情况n/k层,如k=1时退化为O(n),但题目中k≥1且k≤n)。
该方法通过 递归分组 + 局部反转,巧妙地将复杂的链表操作分解为子问题,代码简洁且逻辑清晰,是链表类问题的经典解法。


