大约 3 分钟
这是一道经典的链表问题,考察的是链表的操作技巧,特别是指针的修改和反转。为了满足进阶要求(使用 $O(1)$ 额外内存空间),我们将采用迭代法来实现。
解题思路
这道题可以拆解为以下几个步骤:
- 哨兵节点:为了方便处理头节点翻转后的连接问题,我们创建一个哨兵节点,将其
next指向head。 - 分组遍历:我们需要遍历链表,每次找到一组 $k$ 个节点。
- 在翻转之前,先检查剩余节点是否足够 $k$ 个。如果不够,直接结束。
- 如果足够,记录这组节点的起始位置(
start)和结束位置(end),以及下一组的起始位置。
- 翻转链表:对这 $k$ 个节点进行翻转。
- 重新连接:将翻转后的子链表接回原链表中。
- 前一组的尾节点指向翻转后的头节点。
- 翻转后的尾节点指向下一组的头节点。
- 循环推进:更新指针,继续处理下一组。
JavaScript 代码实现
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} k
* @return {ListNode}
*/
var reverseKGroup = function(head, k) {
// 1. 创建哨兵节点,统一处理头节点翻转的情况
const dummy = new ListNode(0, head);
// before 指向上一组翻转后的尾节点(初始为 dummy)
let before = dummy;
// prevStart 用于遍历链表,指向当前组的第一个节点
let prevStart = head;
while (prevStart) {
// 2. 检查剩余节点是否足够 k 个
let tail = prevStart;
let count = 0;
// 统计剩余节点数量
while (count < k && tail) {
tail = tail.next;
count++;
}
// 如果不足 k 个,保持原有顺序,直接结束
if (count < k) break;
// 3. 翻转 k 个节点
// 翻转后,cur 变为当前组的尾节点,prev 变为当前组的头节点
let prev = null;
let current = prevStart;
// 临时保存下一组的起始位置,翻转结束后 current 会到达这里
let nextGroupStart = tail;
for (let i = 0; i < k; i++) {
// 标准翻转逻辑:保存下一个 -> 改指针 -> 移动 prev -> 移动 current
let nextTemp = current.next;
current.next = prev;
prev = current;
current = nextTemp;
}
// 循环结束后:
// prev 指向翻转后的头节点(原第 k 个节点)
// prevStart 指向翻转后的尾节点(原第 1 个节点)
// 4. 将翻转后的子链表接回原链表
// 上一组的尾节点指向翻转后的头节点
before.next = prev;
// 翻转后的尾节点指向下一组的头节点
prevStart.next = nextGroupStart;
// 5. 更新指针,准备下一轮
// pre 更新为当前组的尾节点
before = prevStart;
// cur 更新为下一组的头节点
prevStart = nextGroupStart;
}
return dummy.next;
};
复杂度分析
- 时间复杂度:$O(n)$,其中 $n$ 是链表的节点总数。虽然我们有两层循环,但内层循环用于检查长度和翻转,每个节点只会被访问常数次。
- 空间复杂度:$O(1)$。我们只使用了几个固定的指针变量(
dummy,pre,cur,prev等),没有使用额外的数组或递归栈空间,满足进阶要求。