【Leetcode Hot 100刷题路线】| 找工作速刷 | 第19题 - [25] - K 个一组翻转链表

题目导航


【Leetcode Hot 100刷题路线】| 找工作速刷 | 第19题 - [25] - K 个一组翻转链表

难度:困难
tags:链表、反转、递归

题目描述


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

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例 1:

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

示例 2:

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

题解


如果你已经掌握了反转链表 I、反转链表 II,那么 K 个一组翻转链表就是它们的自然延伸。

没有刷过这两道题的友友,一定要先做这两道哦,题解点击即可食用:

【Leetcode Hot 100刷题路线】| 找工作速刷 | 第17题- [206] - 反转链表 I

【Leetcode Hot 100刷题路线】| 找工作速刷 | 第18题- [92] - 反转链表 II

主要思路:把链表切成若干段,每段 k 个节点,分别反转

想象你有一条长长的链表,现在要把它切成若干段,每段正好 k 个节点,然后把每一段内部的箭头全部掉头,最后再把所有段按顺序连起来

但这里有个细节:如果最后一段不足 k 个节点,就保持原样,不反转。

为了方便描述,我们依然用之前引入的哨兵节点 dummy 和 p0 指针,p0 永远指向待反转段的前一个节点


第一步:统计链表长度,确定需要反转的组数

我们需要知道链表总共有多少个节点,以便判断能分成几组。
用一个指针遍历链表,统计长度 n,然后需要反转的组数就是 n / k(向下取整)。

int n = 0;
ListNode temp = head;
while (temp != null) {
    n++;
    temp = temp.next;
}

之后我们将用 p0 = dummy 开始处理每一组。

第二步:在每一组内部反转 k 个节点

这一步其实和 反转链表I 一模一样,我们只需要在每一组内执行 k 次箭头掉头操作。

回忆一下反转整个链表的经典代码(pre 初始为 null,cur 为第一个节点):

ListNode pre = null;
ListNode cur = 组内第一个节点;
while (循环 k 次) {
    ListNode nxt = cur.next;   // 保存下一个节点
    cur.next = pre;            // 掉头
    pre = cur;                 // pre 前进
    cur = nxt;                 // cur 前进
}

在本题中,组内第一个节点就是 p0.next,我们把它记为 start。
反转 k 个节点后,pre 会指向反转后的头结点(即原组的最后一个节点),cur 会指向下一组的第一个节点(即原组后面紧跟着的节点)。

第三步:将反转后的组前后连接

反转完一组后,链表实际上被切成了三段:

  • p0 之前的部分(已经处理完)

  • 刚刚反转完的这一组(头是 pre,尾是原来的 start)

  • cur 之后的部分(待处理或保留)

我们需要把这三段重新连接起来,连接顺序非常重要:

  1. 先让本组的尾部(即原来的 start)指向下一组的开头 cur
    因为 start 现在位于组内最后一个位置,它的 next 应该指向后续节点。
p0.next.next = cur;   / p0.next 就是 start,反转后它变成了尾部
  1. 再让 p0 指向本组的头部 pre
    这样 p0 后面的链表就连上了反转后的整组。
p0.next = pre;
  1. 最后移动 p0 到本组的尾部(即原来的 start)
    因为下一组需要一个新的“前一个节点”,而原来的 start 现在变成了本组的最后一个节点,正好可以作为下一组的前一个节点。
p0 = start;   / 或者 p0 = p0.next; 因为 p0.next 已经变成了 pre,但我们需要的是原来的 start

实际上,我们在反转前已经保存了 start = p0.next,所以这里直接用 p0 = start。

第四步:重复处理下一组

每处理完一组,就更新 p0 到新位置,然后继续对下一组进行反转。
直到处理完 n/k 组,剩下的节点(不足 k 个)保持原样,不处理。

演示

我们以 head = [1,2,3,4,5,6,7], k = 3 为例,手动模拟一下前两组的处理过程。

初始状态

dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> null
p0

链表长度 n = 7,需要反转的组数 = 7/3 = 2 组(前两组反转,最后一组剩 1 个不反转)。

1. 第 1 组反转前

dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> null
p0       ↑
       start = p0.next

pre = null, cur = start = 1

反转过程(k=3 次循环):

第一次循环:

  • nxt = cur.next = 2

  • cur.next = pre (null) → 1 -> null

  • pre = cur (1)

  • cur = nxt (2)

结果

dummy     1 -> null     2 -> 3 -> 4 -> 5 -> 6 -> 7 -> null
		  ↑             ↑
p0        pre           cur

第二次循环:

  • nxt = cur.next = 3

  • cur.next = pre (1) → 2 -> 1 -> null

  • pre = cur (2)

  • cur = nxt (3)

dummy    2 -> 1 -> null    3 -> 4 -> 5 -> 6 -> 7 -> null
		 ↑				   ↑
p0       pre               cur

第三次循环:

  • nxt = cur.next = 4

  • cur.next = pre (2) → 3 -> 2 -> 1 -> null

  • pre = cur (3)

  • cur = nxt (4)

dummy    3 -> 2 -> 1 -> null    4 -> 5 -> 6 -> 7 -> null
		 ↑						↑
p0       pre                    cur

反转完成,此时 pre 指向本组新头部(3),cur 指向下一组第一个节点(4),start 仍然是原来的 1(现在在尾部)。

连接:

  • p0.next.next = cur → 1.next = 4(因为 p0.next 是 1)

  • p0.next = pre → dummy.next = 3

  • 移动 p0 到本组尾部:p0 = start = 1(现在 1 是尾部,它的 next 已经是 4)

dummy -> 3 -> 2 -> 1 -> 4 -> 5 -> 6 -> 7 -> null
				   ↑
                   p0

2. 第 2 组反转前
此时 p0 指向 1(上一组的尾部),start = p0.next = 4。

开始第二组反转(同样 k=3 次循环),过程与第一组完全相同,不再赘述。

反转后:

dummy -> 3 -> 2 -> 1 -> 6 -> 5 -> 4 -> 7 -> null
				   ↑    ↑              ↑
                   p0   pre           cur

这里 pre 指向 6,cur 指向 7,start 是原来的 4(现在在尾部)。

连接:

  • p0.next.next = cur → 4.next = 7

  • p0.next = pre → 1.next = 6

  • 移动 p0 到本组尾部:p0 = start = 4

dummy -> 3 -> 2 -> 1 -> 6 -> 5 -> 4 -> 7 -> null
								  ↑
                                  p0

此时已处理完 2 组(n/k = 2),剩余节点不足 k 个,保持原样。最终链表就是 [3,2,1,6,5,4,7]。

可以看到,反转链表 I、反转链表 II、K 个一组翻转链表这三道题的核心都是箭头掉头 + 拼接,区别在于:

  • 反转链表 I:一次性处理整个链表。

  • 反转链表 I:处理连续的一段,用 p0 固定左边界。

  • K 个一组翻转链表:处理多段,每段结束后移动 p0 到新位置。

代码(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) {
        // 1. 统计链表长度
        int n = 0;
        ListNode temp = head;
        while (temp != null) {
            n++;
            temp = temp.next;
        }

        // 2. 设置哨兵节点
        ListNode dummy = new ListNode(0, head);
        ListNode p0 = dummy;  // p0 始终指向待处理组的前一个节点
        ListNode cur = p0.next;
        ListNode pre = null;

        // 3. 循环处理每一组(一共 n/k 组)
        while (n >= k) {
            // 组内反转 k 个节点
            for (int i = 0; i < k; i++) {
                ListNode nxt = cur.next;
                cur.next = pre;
                pre = cur;
                cur = nxt;
            }

            // 此时 pre 指向本组新头部,cur 指向下一组第一个节点
            // 保存本组原来的第一个节点(现在变成尾部)
            ListNode start = p0.next;  

            // 先让本组尾部指向下一组开头
            p0.next.next = cur;
            // 再让 p0 指向本组新头部
            p0.next = pre;
            // 移动 p0 到本组尾部(即原来的 start),为下一组做准备
            p0 = start;

            n -= k;  // 剩余节点数减少 k
        }

        return dummy.next;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值