跳至主要內容

Mr.He大约 3 分钟

这是一道经典的链表问题,考察的是链表的操作技巧,特别是指针的修改和反转。为了满足进阶要求(使用 $O(1)$ 额外内存空间),我们将采用迭代法来实现。

解题思路

这道题可以拆解为以下几个步骤:

  1. 哨兵节点:为了方便处理头节点翻转后的连接问题,我们创建一个哨兵节点,将其 next 指向 head
  2. 分组遍历:我们需要遍历链表,每次找到一组 $k$ 个节点。
    • 在翻转之前,先检查剩余节点是否足够 $k$ 个。如果不够,直接结束。
    • 如果足够,记录这组节点的起始位置(start)和结束位置(end),以及下一组的起始位置。
  3. 翻转链表:对这 $k$ 个节点进行翻转。
  4. 重新连接:将翻转后的子链表接回原链表中。
    • 前一组的尾节点指向翻转后的头节点。
    • 翻转后的尾节点指向下一组的头节点。
  5. 循环推进:更新指针,继续处理下一组。

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 等),没有使用额外的数组或递归栈空间,满足进阶要求。