1. 题目解析与核心思路
这道题目要求我们以k个节点为一组翻转链表,如果剩余节点不足k个则保持原样。乍一看似乎很复杂,但如果我们拆解问题,会发现它其实是基础链表反转操作的进阶应用。
链表反转的核心操作可以类比为"掉头"动作。想象你正在指挥一队士兵行进,突然接到命令要求每k个人为一组向后转。你需要先数出k个人,然后让这k个人依次转身,最后再让整支队伍继续前进。这道题目的难点在于如何精确控制每一组的边界,并在反转后正确连接各组。
2. 解题步骤详解
2.1 准备工作:统计链表长度
在开始反转之前,我们需要知道链表的总长度,这样才能确定需要反转多少组。这个步骤很简单,只需要遍历整个链表并计数即可。
java复制int n = 0;
ListNode temp = head;
while (temp != null) {
n++;
temp = temp.next;
}
这个操作的时间复杂度是O(n),空间复杂度是O(1)。虽然看起来有些多余,但它能帮助我们避免在后续处理中频繁检查边界条件。
2.2 设置哨兵节点
在处理链表问题时,使用哨兵节点(dummy node)是一个常用技巧。它可以简化边界条件的处理,特别是在头节点可能发生变化的情况下。
java复制ListNode dummy = new ListNode(0, head);
ListNode p0 = dummy; // p0始终指向当前处理组的前一个节点
哨兵节点的值可以是任意值,因为它不会被实际使用。它的存在让我们可以统一处理所有节点,不需要为头节点单独写特殊逻辑。
2.3 分组反转的核心逻辑
这是整个算法最核心的部分。我们需要循环处理每一组k个节点,执行反转操作。关键点在于:
- 每次反转k个节点
- 正确连接反转后的子链表
- 更新指针位置为下一组做准备
java复制while (n >= k) {
ListNode pre = null;
ListNode cur = p0.next;
ListNode start = cur; // 保存当前组的第一个节点
// 反转k个节点
for (int i = 0; i < k; i++) {
ListNode nxt = cur.next;
cur.next = pre;
pre = cur;
cur = nxt;
}
// 连接反转后的子链表
p0.next.next = cur; // 原第一个节点现在指向下一组的第一个节点
p0.next = pre; // 前一组最后一个节点指向当前组的新头节点
p0 = start; // 移动p0到当前组的最后一个节点(反转前的第一个节点)
n -= k; // 更新剩余节点数
}
这个循环会一直执行,直到剩余的节点数不足k个。每次循环处理一组k个节点,反转后正确连接,然后移动指针准备处理下一组。
3. 关键点解析与注意事项
3.1 指针移动的顺序
在连接反转后的子链表时,指针移动的顺序非常重要。错误的顺序可能导致链表断裂或形成环。正确的顺序应该是:
- 先让反转后的子链表的尾部指向下一组的头部
- 然后让前一组的尾部指向当前组的新头部
- 最后移动p0指针到当前组的尾部
3.2 边界条件处理
虽然我们提前统计了链表长度,但在实际编码时仍需注意以下边界条件:
- 空链表输入
- k=1的情况(实际上不需要任何操作)
- k等于链表长度的情况(相当于反转整个链表)
- 链表长度不是k的整数倍的情况
3.3 时间复杂度分析
整个算法的时间复杂度是O(n),其中n是链表长度。我们最多遍历链表两次:一次统计长度,一次执行反转操作。空间复杂度是O(1),只使用了固定数量的指针变量。
4. 完整代码实现
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) {
// 特殊情况处理
if (head == null || k == 1) return head;
// 1. 统计链表长度
int n = 0;
ListNode temp = head;
while (temp != null) {
n++;
temp = temp.next;
}
// 2. 设置哨兵节点
ListNode dummy = new ListNode(0, head);
ListNode p0 = dummy;
// 3. 分组反转
while (n >= k) {
ListNode pre = null;
ListNode cur = p0.next;
ListNode start = cur;
// 反转k个节点
for (int i = 0; i < k; i++) {
ListNode nxt = cur.next;
cur.next = pre;
pre = cur;
cur = nxt;
}
// 连接反转后的子链表
p0.next.next = cur;
p0.next = pre;
p0 = start;
n -= k;
}
return dummy.next;
}
}
5. 常见问题与调试技巧
5.1 链表断裂问题
在调试时,最常见的错误是链表断裂。这通常发生在连接反转后的子链表时指针操作顺序不正确。可以通过以下方法检查:
- 打印每个关键步骤后的链表状态
- 特别注意p0、pre、cur等指针的位置
- 检查是否有节点的next指针意外变为null
5.2 循环链表问题
另一个常见错误是意外创建了循环链表。这通常发生在反转时没有正确保存next指针。建议:
- 在反转操作前总是先保存next指针
- 使用小规模测试用例(如k=2)手动模拟执行过程
5.3 性能优化
虽然这个解法已经是O(n)时间复杂度,但在实际面试中可以讨论以下优化点:
- 如果k很大,可以提前判断剩余节点是否足够,避免不必要的遍历
- 对于特别长的链表,可以考虑并行处理不同的组(虽然这会增加空间复杂度)
6. 扩展思考
这道题目很好地展示了如何将基础算法(链表反转)应用到更复杂的问题中。类似的思路还可以用于:
- 每隔k个节点执行特定操作
- 分组统计链表信息
- 分块处理大数据链表
掌握这种"分而治之"的思想对解决许多链表问题都非常有帮助。在实际工程中,这种分组处理的思想也常用于批处理、分页等场景。