力扣解题-25. K 个一组翻转链表

26 阅读7分钟

力扣解题-25. K 个一组翻转链表

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例 1:

image.png

输入:head = [1,2,3,4,5], k = 2

输出:[2,1,4,3,5]

示例 2:

image.png

输入:head = [1,2,3,4,5], k = 3

输出:[3,2,1,4,5]

提示:

链表中的节点数目为 n

1 <= k <= n <= 5000

0 <= Node.val <= 1000

进阶:你可以设计一个只用 O(1) 额外内存空间的算法解决此问题吗?

Related Topics

递归、链表


第一次解答

解题思路

核心方法:迭代分块翻转法(O(1)空间),通过哑节点+分块处理的思路,将链表按k个节点为一组拆分,逐组翻转后重新拼接,剩余不足k个的节点保持原顺序,满足进阶的O(1)额外空间要求,是本题的最优迭代解法。

核心逻辑拆解

K个一组翻转链表的核心是“分块→翻转→拼接”,关键在于精准定位每组的边界并处理拼接逻辑:

  1. 边界处理:若链表为空(head==null)或k=1(无需翻转),直接返回原链表;
  2. 哑节点初始化:创建dummy哑节点(dummy.next=head),prevGroupEnd指针初始指向dummy(作为上一组翻转后的尾节点);
  3. 循环分块处理
    • 定位当前组尾节点end指针从prevGroupEnd开始遍历k步,若中途end==null(剩余节点不足k个),直接返回结果;
    • 标记关键节点
      • start:当前组的头节点(prevGroupEnd.next);
      • nextGroupStart:下一组的头节点(end.next);
    • 断开当前组:将end.next置为null(便于独立翻转当前组);
    • 翻转当前组:调用reverseList翻转startend的节点,翻转后end变为当前组新头,start变为当前组新尾;
    • 重新拼接链表
      • prevGroupEnd.next = end(上一组尾节点连接当前组新头);
      • start.next = nextGroupStart(当前组新尾连接下一组头节点);
    • 更新指针prevGroupEnd = start(将当前组新尾作为下一组的“上一组尾节点”);
  4. 返回结果:循环结束后返回dummy.next(跳过哑节点)。
具体步骤(以示例1 head=[1,2,3,4,5]、k=2为例)
步骤操作关键指针状态链表状态
1初始化dummy→1→2→3→4→5prevGroupEnd=dummy-
2定位end=2(k=2步)end=2, start=1, nextGroupStart=3-
3断开end.next=nullend.next=nulldummy→1→2(独立组)
4翻转当前组翻转后1←2-
5拼接:prevGroupEnd.next=2dummy→2→1-
6拼接:start.next=3dummy→2→1→3→4→5-
7更新prevGroupEnd=1prevGroupEnd=1-
8定位end=4(k=2步)end=4, start=3, nextGroupStart=5-
9重复步骤3-7-dummy→2→1→4→3→5
10定位end=5(仅1步,不足k)end=null,返回结果最终链表:2→1→4→3→5
性能说明
  • 时间复杂度:O(n)(每个节点仅被访问两次:一次分块遍历,一次翻转,n为节点总数);
  • 空间复杂度:O(1)(仅使用指针变量,无额外数据结构),满足进阶要求;
  • 优势:
    1. 纯迭代实现,无递归栈开销,空间效率最优;
    2. 分块处理逻辑清晰,边界条件(不足k个节点)天然处理;
    3. 哑节点避免了头节点翻转的特殊处理。
    public ListNode reverseKGroup(ListNode head, int k) {
        if(head==null || k==1){
            return head;
        }
        ListNode dummy = new ListNode(0);
        dummy.next=head;

        ListNode prevGroupEnd=dummy;
        while(true){
            //找到当前组的尾巴节点
            ListNode end=prevGroupEnd;
            for(int i=0;i<k;i++){
                end=end.next;
                if(end==null){
                    return dummy.next;
                }
            }
            //当前组第一个节点
            ListNode start=prevGroupEnd.next;
            //下一组反转的起点
            ListNode nextGroupStart=end.next;

            //断开尾巴
            end.next=null;

            //开始反转
            reverseList(start);

            //重新连接三段
            prevGroupEnd.next=end;//end是新的头
            start.next=nextGroupStart;//start是新的尾巴,连上下一组

            //继续下一组
            prevGroupEnd=start;
        }
    }

    public ListNode reverseList(ListNode head) {
        if (head == null) {
            return head;
        }
        ListNode pre = null;
        ListNode cur = head;
        while (cur != null) {
            ListNode temp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = temp;
        }
        return pre;
    }

示例解答

解题思路

解法1:递归法(思路简洁,空间O(n/k))

核心方法:递归分治翻转,将“K个一组翻转”拆解为“翻转当前组 + 递归翻转剩余组”,逻辑更简洁但递归栈深度为n/k(空间复杂度O(n/k)),不满足进阶的O(1)空间要求,但易于理解。

代码实现
public ListNode reverseKGroup(ListNode head, int k) {
    if (head == null || k == 1) {
        return head;
    }
    // 步骤1:检查当前组是否有k个节点
    ListNode curr = head;
    int count = 0;
    while (curr != null && count < k) {
        curr = curr.next;
        count++;
    }
    // 不足k个,直接返回原头
    if (count < k) {
        return head;
    }
    
    // 步骤2:翻转当前组的k个节点
    ListNode pre = null;
    ListNode cur = head;
    count = 0;
    while (cur != null && count < k) {
        ListNode temp = cur.next;
        cur.next = pre;
        pre = cur;
        cur = temp;
        count++;
    }
    
    // 步骤3:递归翻转剩余组,当前组尾节点(原head)连接剩余组的头
    head.next = reverseKGroup(cur, k);
    
    // 返回当前组的新头(pre)
    return pre;
}
核心逻辑说明
  1. 递归终止条件
    • 链表为空或k=1,直接返回;
    • 当前组节点不足k个,返回原头节点(剩余节点不翻转);
  2. 翻转当前组:手动翻转前k个节点,pre变为当前组新头,cur指向剩余组头;
  3. 递归拼接:当前组原头节点(head)的next指向剩余组翻转后的头节点;
  4. 返回结果:返回当前组新头(pre)。
性能说明
  • 时间复杂度:O(n)(每个节点仅被访问一次);
  • 空间复杂度:O(n/k)(递归栈深度为分组数,n/k);
  • 优势:代码极简,递归分治思路符合直觉,无需处理复杂的指针拼接;
  • 劣势:递归栈有额外空间开销,不满足进阶的O(1)空间要求。
解法2:迭代法优化(原地翻转,无需断开组)

核心方法:原地翻转优化,在原迭代法基础上,无需断开当前组(去掉end.next=null),直接在原链表中翻转k个节点,减少指针操作步骤,代码更简洁。

代码实现
public ListNode reverseKGroup(ListNode head, int k) {
    if (head == null || k == 1) {
        return head;
    }
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    ListNode prevGroupEnd = dummy;
    ListNode curr = head;
    int count = 0;
    
    while (curr != null) {
        count++;
        // 每累计k个节点,翻转一次
        if (count == k) {
            // 翻转prevGroupEnd.next 到 curr 的k个节点
            prevGroupEnd = reverseBetween(prevGroupEnd, curr.next);
            // 重置计数,curr指向新的起始节点
            curr = prevGroupEnd.next;
            count = 0;
        } else {
            curr = curr.next;
        }
    }
    return dummy.next;
}

// 翻转from.next 到 to.prev 的节点,返回翻转后的尾节点
private ListNode reverseBetween(ListNode from, ListNode to) {
    ListNode pre = from;
    ListNode cur = from.next;
    ListNode start = cur; // 翻转后的尾节点
    while (cur != to) {
        ListNode temp = cur.next;
        cur.next = pre;
        pre = cur;
        cur = temp;
    }
    // 拼接:from连接新头(pre),原头(start)连接to
    from.next = pre;
    start.next = to;
    return start;
}
核心逻辑说明
  1. 计数分块:遍历链表时累计节点数,每达到k个节点触发一次翻转;
  2. 原地翻转:调用reverseBetween直接翻转from.nextto.prev的k个节点,无需断开链表;
  3. 返回尾节点reverseBetween返回翻转后的尾节点,作为下一组的prevGroupEnd
  4. 优势:减少“断开-翻转-拼接”的步骤,指针操作更高效,代码更简洁。
性能说明
  • 时间复杂度:O(n)(与原迭代法一致);
  • 空间复杂度:O(1)(与原迭代法一致);
  • 优势:无需断开链表,减少指针操作次数,执行效率略高;
  • 劣势:reverseBetween函数增加了代码抽象度,新手理解稍难。

总结

  1. 迭代分块翻转法(第一次解答):O(n)时间+O(1)空间,满足进阶要求,分块逻辑清晰,是工程首选的最优解法;
  2. 递归分治翻转法:O(n)时间+O(n/k)空间,代码极简、易理解,但有递归栈开销,不满足进阶要求;
  3. 迭代原地翻转优化版:O(n)时间+O(1)空间,减少指针操作步骤,执行效率更高,代码稍抽象;
  4. 关键技巧:
    • 核心思想:K个一组翻转的本质是“分块处理+局部翻转+链表拼接”,哑节点是处理头节点的关键;
    • 空间优化:纯迭代法可实现O(1)空间,递归法易理解但有栈开销;
    • 边界处理:遍历分块时需检查剩余节点是否≥k,不足则直接返回,保证剩余节点顺序不变。