一、题目描述
将给出的链表中的结点每 k 个一组翻转,返回翻转后的链表。
如果链表中的结点数不是 k 的倍数,将最后剩下的结点保持原样。
你不能更改结点中的值,只能更改结点本身。
二、题目解析
对于这道题目,如果一开始摸不清思路,可以先模拟看看,以 k 为 3 作为示例。
通过上图可以注意到,每次操作的过程实际上就是在翻转准备翻转链表的区域,把这个区域的所有结点都翻转成功之后,再连接到已经翻转区域链表的区域尾部,然后再继续去还未翻转的区域里面寻找出 k 个结点来继续操作。
完整的操作可以查看下方的动画。
接下来,我们从头到尾模拟一下这个过程是怎么操作的。
1、对于原链表来说,除了头结点之外,其余的所有结点都有前面一个结点指向它,那么为了避免在操作这些结点(比如移动、删除操作)过程中需要判断当前结点是否是头结点,可以在原链表的前面增加一个虚拟结点,这样就可以使得原链表里面的所有结点的地位都是一样的了。
2、接下来,设置两个指针,都指向虚拟头结点的位置,一个指针是 pre,它在后续的操作过程中会始终指向每次要翻转的链表的头结点的【上一个结点】,另外一个指针是 end,它在后续的操作过程中会始终指向每次要翻转的链表的尾结点。
3、以 k 为 3 作为示例,那么 pre 和 end 不断的向后移动,当 end 来到 3 这个结点的时候,已经寻找出来需要翻转的那 3 个结点,并且 pre 依旧指向 dummy,因为 1 这个结点是翻转区域的头结点,前面那个则是上一个结点,因此 pre 还停留在原地。
4、那么需要去翻转 1 、2 、3 这个结点,为了避免翻转过程会影响其它区域的结点,这个时候就需要把翻转区域和其它区域断开连接,同时又为了保证翻转成功之后能顺利连接回去,因此需要记录一下后续的结点,这个结点也就是 end.next ,接下来再断开准备翻转链表区域的前后区域,如下图所示。
5、而 1 、2 、3 的翻转过程可以直接套用反转链表这题的思路和代码。
6、该区域的结点翻转成功之后,连接回去,回到第 2 点提到的 pre 这个指针是始终指向每次要翻转的链表的头结点的【上一个结点】,实际上也就是已经翻转区域的尾结点位置,所以 pre 需要来到 end 的位置,即 1 这个结点位置。
7、接下来,继续让 end 向后移动,找到 k 个结点来。
8、每次 end 寻找出 k 个结点之后,都会执行同样的逻辑,记录接下来需要翻转区域的头结点, 断开前后的区域,翻转本区域的结点,再让 pre 来到 end 的位置,end 来到 start 的位置。
9、最后,直到 end 遍历不到 k 个结点或者指向了 null 的时候也就完成了整个翻转过程。
三、参考代码
import java.util.*;
// 本题文字版详解请访问下方网站
// https://www.algomooc.com
// 作者:程序员吴师兄
public class Solution {
/**
*
* @param head ListNode类
* @param k int整型
* @return ListNode类
*/
public ListNode reverseKGroup (ListNode head, int k) {
// 一开始设置一个虚拟结点,它的值为 -1,它的值可以设置为任何的数,因为我们根本不需要使用它的值
ListNode dummy = new ListNode(-1);
// 虚拟头结点的下一结点指向 head 结点
// 如果原链表是 1 --> 2 --> 3
// 那么加上虚拟头结点就是 -1 --> 1 --> 2 --> 3
dummy.next = head;
// 设置一个指针,指向此时的虚拟结点,pre 表示每次要翻转的链表的头结点的【上一个结点】
// pre: -1 --> 1 --> 2 --> 3
ListNode pre = dummy;
// 设置一个指针,指向此时的虚拟结点,end 表示每次要翻转的链表的尾结点
// end: -1 --> 1 --> 2 --> 3
ListNode end = dummy;
// 通过 while 循环,不断的找到翻转链表的尾部
while ( end.next != null) {
// 通过 for 循环,找到【每一组翻转链表的尾部】
// 由于原链表按照 k 个一组进行划分会可能出现有一组的长度不足 k 个
// 比如原链表 1 --> 2 --> 3 --> 4 --> 5
// k = 2,划分了三组 1 --> 2, 3 --> 4, 5
// 所以得确保 end 不为空才去找它的 next 指针,否则 null.next 会报错
for (int i = 0 ; i < k && end != null ; i++) {
// end 不断的向后移动,移动 k 次到达【每一组翻转链表的尾部】
end = end.next ;
}
// 如果发现 end == null,说明此时翻转的链表的结点数小于 k ,保存原有顺序就行
if (end == null) {
// 直接跳出循环,只执行下面的翻转操作
break;
}
// next 表示【待翻转链表区域】里面的第一个结点
ListNode next = end.next;
// 【翻转链表区域】的最尾部结点先断开
end.next = null ;
// start 表示【翻转链表区域】里面的第一个结点
ListNode start = pre.next;
// 【翻转链表区域】的最头部结点和前面断开
pre.next = null;
// 这个时候,【翻转链表区域】的头结点是 start,尾结点是 end
// 开始执行【反转链表】操作
// 原先是 start --> ...--> end
// 现在变成了 end --> ...--> start
// 要翻转的链表的头结点的【上一个结点】的 next 指针指向这次翻转的结果
pre.next = reverseList(start);
// 接下来的操作是在为【待翻转链表区域】的反转做准备
// 原先是 start --> ...--> end
// 现在变成了 end --> ...--> start
// 【翻转链表区域】里面的尾结点的 next 指针指向【待翻转链表区域】里面的第一个结点
start.next = next ;
// 原先是 start --> ...--> end
// 现在变成了 end --> ...--> start
// pre 表示每次要翻转的链表的头结点的【上一个结点】
pre = start;
// 将 end 重置为【待翻转链表区域】的头结点的上一个结点。
end = start;
}
return dummy.next;
}
private ListNode reverseList(ListNode head) {
// 寻找递归终止条件
// 1、head 指向的结点为 null
// 2、head 指向的结点的下一个结点为 null
// 在这两种情况下,反转之后的结果还是它自己本身
if (head == null || head.next == null) return head;
// 不断的通过递归调用,直到无法递归下去,递归的最小粒度是在最后一个结点
// 因为到最后一个结点的时候,由于当前结点 head 的 next 结点是空,所以会直接返回 head
ListNode cur = reverseList(head.next);
// 比如原链表为 1 --> 2 --> 3 --> 4 --> 5 --> null
// 5 -- > 4
// 第一次执行下面代码的时候,head 为 4,那么 head.next = 5
// 那么 head.next.next 就是 5.next ,意思就是去设置 5 的下一个结点
// 等号右侧为 head,意思就是设置 5 的下一个结点是 4
head.next.next = head;
// head 原来的下一结点指向自己,所以 head 自己本身就不能再指向原来的下一结点了
// 否则会发生无限循环
head.next = null;
// 我们把每次反转后的结果传递给上一层
return cur;
}
}