上回说到的是单链表反转的“二哥” —— 两两交换链表中的节点。今天来分析一下它的大哥:“K个一组翻转链表”。
题目描述如下。
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
示例:
给你这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5
如果一上来就做K个一组翻转链表这个题,难度还是比较大的,如果前面有了它两个弟弟的铺垫,那么这道题目就是水到渠成的了,前面两道题目的答案结合起来就是这道题目的答案了。这道题目和它两个兄弟一样,也可以使用迭代和递归两种方法解决。
方法一:三指针迭代
有了上回单链表反转的双指针迭代法的基础,再来解决它二哥,就相对容易很多了。
“巧妇难为无米之炊”。题目中说的是K个一组翻转,那首先得有K个节点才行,和“两两交换链表中的节点”一样,上来先判断够K个节点才继续交换,而且循环继续的条件也是要满足剩下的节点够K个才行。
我们可以使用两个变量left
和right
来表示当前要进行翻转的一段链表中的起始节点和结束节点。
如上图,指针L
和R
分别表示一段要进行翻转的链表中的起始节点和结束节点,如何找到一段链表中的第K个节点,我们可以写一个方法来实现,如果剩下的链表的长度够K个,就返回第K个节点的指针,反之返回null
:
private ListNode findKthNode(ListNode p, int k) {
int curCount = 0;
while (curCount < k - 1 && p != null) {
p = p.next;
curCount++;
}
return (curCount == k - 1) ? p : null;
}
指针L
和R
之间的节点怎么进行翻转,和链表反转全家桶(一):动画详解单链表反转中的方法是一样的,唯一区别是“单链表反转”中结束的指针是尾指针,而这里结束的指针是指定的 —— 每段链表的第K个节点。有迭代和递归两种,为了保持方法的一致性,这里还用迭代法。实现代码如下:
private void reverseRange(ListNode left, ListNode right) {
if (left == right) {
return;
}
ListNode exit = right.next;
ListNode cur = left, pre = null;
while (cur != exit) {
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
}
同时,要考虑下面的这种场景,第一段的K个节点已经完成翻转,现在轮到第二段链表进行翻转,完成之后,pre
需要指向R
节点,所以还要记录每一轮交换的前驱节点,下图这个例子里面前驱节点就是节点1。
整体的代码实现如下:
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null || head.next == null) {
return head;
}
ListNode pre = null, left = head, right = findKthNode(head, k);
if (right == null) {
return head;
}
ListNode ans = right;
while (left != null && right != null) {
ListNode next = right.next;
reverseRange(left, right);
left.next = next;
if (pre != null) {
pre.next = right;
}
pre = left;
left = next;
right = findKthNode(next, k);
}
return ans;
}
private void reverseRange(ListNode left, ListNode right) {
if (left == right) {
return;
}
ListNode exit = right.next;
ListNode cur = left, pre = null;
while (cur != exit) {
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
}
private ListNode findKthNode(ListNode p, int k) {
int curCount = 0;
while (curCount < k - 1 && p != null) {
p = p.next;
curCount++;
}
return (curCount == k - 1) ? p : null;
}
}
观察一下上面的代码,它和“两两交换链表中的节点”的结构是一样的。下图为代码对比,可以看到1,2,3处的代码模式是一样的,“两两交换链表的节点”是“K个一组翻转链表”在K=2
时的特例。
复杂度分析
- 时间复杂度:
O(n)
。 - 空间复杂度:
O(1)
。
方法二:递归
递归法同样是它的两个小弟的结合。
一个链表段内的翻转的递归写法和“单链表的翻转”是一样的,唯一不同点是递归退出条件不同,链表段的翻转递归退出条件是左指针和右指针相遇,而单链表反转的退出条件是当前节点为空或当前节点已经是尾节点。
同样,它的整体写法和“两两交换链表中的节点”是一样的,“两两交换链表中的节点”是“K个一组翻转链表”在K为2时的特例。
代码如下:
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode left = head, right = findKthNode(head, k);
if (right == null) {
return left;
}
ListNode next = reverseKGroup(right.next, k);
reverseRange(left, right);
left.next = next;
return right;
}
private void reverseRange(ListNode left, ListNode right) {
if (left == right) {
return;
}
reverseRange(left.next, right);
left.next.next = left;
left.next = null;
}
private ListNode findKthNode(ListNode p, int k) {
int curCount = 0;
while (curCount < k - 1 && p != null) {
p = p.next;
curCount++;
}
return (curCount == k - 1) ? p : null;
}
}
复杂度分析
- 时间复杂度:
O(n)
。 - 空间复杂度:
O(n)
,使用了递归,递归的栈深度为n
。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。