先记住这个答案
先用哑节点固定头位置,遍历统计每组是否有 k 个节点;若满 k 个则反转该子链表并接回,否则停止。实现时每个子链表反转前记录前驱和后继,反转完成后更新指针;时间复杂度 O(n),额外空间 O(1)。
- 先检查剩余节点数是否满 k
- 反转子链表需保存前驱后继
- 剩余不足k个保持原序
分组反转的核心流程
采用哑节点简化头部处理。用指针 prev 指向待处理组的前驱,每次先走 k 步检查是否存在 k 个节点:若中途遇空,说明不足,直接返回 dummy.next。若足够,则记录该组起点 start 和组后第一个节点 nextGroup。
反转 start 到 nextGroup 之前的子链表,此时 start 会成为该组的新尾部。将 prev.next 指向反转后的新头,同时把 start.next 接在 nextGroup 上。随后更新 prev 为 start,继续下一轮,直到后续剩余不足 k。
function reverseKGroup(head, k) {
const dummy = new ListNode(0, head);
let prev = dummy;
while (prev) {
let groupHead = prev.next;
let end = prev;
// 检查是否还有 k 个节点
for (let i = 0; i < k && end; i++) end = end.next;
if (!end) break; // 不足 k 个,保留原序
const nextGroup = end.next;
// 反转 groupHead 到 end 之间的子链表
let cur = groupHead;
let last = null;
let temp = null;
while (cur !== nextGroup) {
temp = cur.next;
cur.next = last;
last = cur;
cur = temp;
}
// 此时 last 是反转后的新头,groupHead 变成新尾
prev.next = last;
groupHead.next = nextGroup;
prev = groupHead;
}
return dummy.next;
}
// 辅助测试用
function toArray(head) {
const res = [];
while (head) { res.push(head.val); head = head.next; }
return res;
}
function fromArray(arr) {
const dummy = new ListNode();
let tail = dummy;
for (const val of arr) { tail.next = new ListNode(val); tail = tail.next; }
return dummy.next;
}
function ListNode(val, next) {
this.val = (val===undefined ? 0 : val);
this.next = (next===undefined ? null : next);
}
// 测试:k=3,链表长度 5 -> 最后不足 3 保留
console.log(JSON.stringify(toArray(reverseKGroup(fromArray([1,2,3,4,5]), 3))));
// 预期输出 [3,2,1,4,5]
// 测试:k=2,链表长度 3 -> 最后不足 2 保留
console.log(JSON.stringify(toArray(reverseKGroup(fromArray([1,2,3]), 2))));
// 预期输出 [2,1,3]
// 测试:k=1,所有组都只有1个,无反转
console.log(JSON.stringify(toArray(reverseKGroup(fromArray([1,2,3]), 1))));
// 预期输出 [1,2,3]查看输出与解释
[3,2,1,4,5]
[2,1,3]
[1,2,3]该代码在 Node.js 中直接运行,输出为上述结果。每轮循环先检查是否有 k 个节点,确认后反转子链表,再调整前后连接,时间复杂度 O(n),空间 O(1)。
日志序列按批倒序处理
假设有一个日志列表,按时间顺序存储,每批最多处理 3 条,要求批内倒序,最后不足 3 条则不打乱。例如有 5 条日志 [L1,L2,L3,L4,L5],应输出 [L3,L2,L1,L4,L5],因为 [L4,L5] 不足 3 条不反转。
使用分组反转算法,先检查每批是否有 3 个节点。如果有则批内反转,并维护好相邻批次的链指针;如果没有则直接返回当前头。这样只需要一次遍历且不修改节点值,通过调整指针实现反转,避免复制节点数据。
边界判定与失败条件
最容易出错的是 k 等于链表长度或 k 大于长度。当 k 大于长度时,第一次检查就会遇到 null,循环不会执行,直接返回原链表,正确。当 k 等于长度时,整个链表反转一次,最后 prev 会移动到原链表尾,循环再次检查时剩余 0 个节点,也不反转,结果正确。
另一个风险是反转操作中的指针错乱:反转子链表时若错误修改 nextGroup 前的连接,会导致后半部分丢失。因此必须记录 nextGroup 并在反转后立刻连接。此外,当 k=1 时,循环每次检查都满足,但反转子链表长度为 1,不会改变顺序,相当于一次遍历,性能仍为 O(n)。
容易答错的地方
- 先反转再判断不足 k
- 若先反转再判断,可能对不足 k 的子链也反转,导致顺序破坏。正确做法是每次循环前先走 k 步检查是否存在足够节点,不足则直接结束。
- 忘记更新前驱指针
- 每一组反转完成后,必须把前驱
prev移动到当前组的尾部(即原起点),否则下一组会从错误位置连接,导致链表断裂或重复。
面试官还会怎么问?
如果要求不足 k 个时也反转,如何修改?
需要调整边界判断:每次循环先检查剩余节点数是否小于 k,若小于则反转剩余全部(将 nextGroup 置为 null,即反转从当前起点到链表尾的所有节点);若满 k 则仍按 k 步反转。不能简单去掉检查,因为需要确定反转终点。
递归实现和迭代哪一种更安全?
迭代更不易栈溢出,空间 O(1);递归代码简洁但递归深度可能达到 n/k,在链表很长时风险高。实际应用建议迭代。
当 k 很大且链表较短时,时间复杂度会退化吗?
仍为 O(n),因为每次检查走 k 步,总步数为 n;短链表时走完一遍发现不足,依然线性。额外操作为常数,没有退化。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。