1. 题目来源
题单:
-
- 链表、二叉树与一般树(前后指针/快慢指针/DFS/BFS/直径/LCA)
- §1.4 反转链表
2. 题目解析
前导题:
链表题目建议画图理解,且是个大热门…
本题和上题一样,都是拿指针 p
定位,然后去处理后面的情况。上题是处理后面两个节点的反转情况,并更新指针 p
。本题是处理后面 k
个节点的反转情况,并更新指针 p
。故唯一的区别就是反转次数变多了,对于指针的操作及链表反转的内部细节原理更加苛刻。
在此拿 p
定位,再处理后面的情况,能有效的将虚拟头结点利用起来,达成代码的统一,并且对于单链表题目而言,知道待处理节点的上一个节点是重要的。想不通多做点题就想通了。
思路:
- 建立虚拟头结点,其
next
指向头结点。 - 定位点
p
一开始指向虚拟头结点,准备反转后面的k
链表节点。 - 首先判断后面有没有
k
个节点。没有的话直接返回即可。 - 其次,采用三指针原地逆置的方法,局部反转这
k
个链表节点。循环结束时,a
指针应该在第k
个节点,b
指针应该在第k+1
个节点。只需要反转k-1
次,即改变k-1
个next
指针指向关系即可。 - 最后处理前后连接问题。此时
p
指针仍在定位点处,且指向反转前的第一个节点,该节点的next
应该要指向b
节点,然后p
节点的next
应该指向a
节点,做新的定位,用来更新下一组k
个节点。
其实,这里 y 总和 灵神 这里都遗漏了一点,即第二次反转的时候,a 指针的所处位置是不对的。希望和认知是 a 应该在反转区间的前一个指针。而第一次反转后,a 指针被反转到第一个区间的头部位置,且没有被纠正回来。
但进行第二轮翻转的时候,b->next = a,这里实际上指向了第一个区间的头部位置。然后 a=b, b= c 就被纠正过来了。后续的就不会出现这个问题了。
同时,反转区间的第一个指针的 next 出了循环后有 p0->next->next = b 就会被马上更改为 下一个区间的头部位置。所以 b->next=a 即便指向错误也没有任何影响。因为在此没有实际的作用。
注意:
- 链表反转的本质。反转次数,最终指针指向位置。
- 最后的细节处理,需要先将反转区间的前一个节点存下来。这样就可以改变翻转区间第一个节点的指向位置。即 p->next->next = b 即对它进行了首尾替换。且 p->next=a 将它在指向反转区间的最后一个元素,即可完成整个区间的翻转、翻转后的链表链接。
- 时间复杂度: O ( n ) O(n) O(n)。
- 空间复杂度: O ( 1 ) O(1) O(1)
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
auto dummy = new ListNode(-1);
dummy->next = head;
for (auto p = dummy; ;) { // p开始指向虚拟头结点,做定位点
auto q = p;
for (int i = 0; i < k && q; ++i) q = q->next; // 判断后面是否有k个
if (!q) break;
auto a = p->next, b = a->next;
for (int i = 0; i < k - 1; ++i) { // 链表原地逆置,只需要反转k-1次
auto c = b->next;
b->next = a;
a = b, b = c;
}
auto c = p->next; // 处理首尾连接情况
p->next = a, c->next = b;
p = c;
}
return dummy->next;
}
};
灵神的写法:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
int n = 0;
for (auto p = head; p; p = p->next) n ++ ;
auto dummy = new ListNode(-1, head);
auto p0 = dummy, a = dummy, b = a->next;
for (; n >= k; n -= k) {
for (int j = 0; j < k; j ++ ) {
auto c = b->next;
b->next = a;
a = b, b = c;
}
auto next = p0->next;
p0->next->next = b; // 其实这里会把链表起始的 next 指针重新赋值。本来一开始指向的上一轮的 a,但是会重新赋值为 b
p0->next = a;
a = p0 = next; // 这样写可能更加合理。a 应该是待翻转链表的前一个位置
}
return dummy->next;
}
};