25. K 个一组翻转链表
class Solution {
ListNode *Reverse(ListNode *Head, int Length) {
ListNode *RevHead = nullptr;
ListNode *FwdHead = Head;
while (Length--) {
ListNode *FwdNext = FwdHead->next;
FwdHead->next = RevHead;
RevHead = FwdHead;
FwdHead = FwdNext;
}
return RevHead;
}
public:
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode *Tracking = head;
// Tracking is at tail after this iteration process.
for (int i = 0; i < k; ++i) {
if (!Tracking)
return head;
Tracking = Tracking->next;
}
ListNode Dummy(0, head);
Dummy.next = Reverse(Dummy.next, k);
// At this time, head has been moved to tail.
head->next = reverseKGroup(Tracking, k);
return Dummy.next;
}
};
这个题目用递归写思路是非常非常清晰易懂的,唯一的问题就是一定要记清楚翻转链表应该怎么写。
二刷
class Solution {
ListNode *Reverse(ListNode *head) {
ListNode *Prev = nullptr;
while (head) {
ListNode *Next = head->next;
head->next = Prev;
Prev = head;
head = Next;
}
return Prev;
}
public:
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode *NewHead = head;
ListNode *Prev = nullptr;
while (true) {
ListNode *Next = head;
ListNode DNextPrev(0, Next), *NextPrev = &DNextPrev;
// Advance Next for next round of reverse operation.
for (int i = 0; i < k; ++i) {
if (!Next) {
// Not divisible, just connect and return.
if (Prev)
Prev->next = head;
return NewHead;
}
NextPrev = NextPrev->next;
Next = Next->next;
}
// Break the link in order to Reverse().
NextPrev->next = nullptr;
ListNode *RevHead = Reverse(head);
// Nullity check for Prev, and then assign head to it in order to connect
// two consecutive groups in the next iteration.
if (Prev)
Prev->next = RevHead;
Prev = head;
// NewHead only need to be assigned for once.
if (NewHead == head)
NewHead = RevHead;
head = Next;
}
}
};
翻转链表又差点没写出来,我服了。我甚至好像二刷的时候把这个东西给忘了?我真得回去再多写两遍。
不过说实话我这个写得有点太复杂了,第一是我的 Reverse() 是 null-terminated 的,而不是接受头尾、返回头尾的。但其实我之前写合并的时候踩过这个坑啊,我没想到这里用头尾写法竟然是可以的?
第二是我对 Dummy 的理解还是不够深刻——Dummy 的意义一方面在于作为 Prev 的载体,另一方面,基于 Prev 的特性,它也有承载头、对头进行修改的能力。
第三是我对 Prev 的理解也不够到位。我老是想着「Prev 和 Curr 不是一个东西」,殊不知其实 Curr == Prev->next。我服了,灯下黑。
class Solution {
std::pair<ListNode *, ListNode *> Reverse(ListNode *First, ListNode *Last) {
// In fact we can also just default construct. Either way the list needs to
// be manually put back into original, because we need the NEWEST head
// instead of this old, not reversed head.
ListNode *Prev = Last->next;
ListNode *Curr = First;
while (Prev != Last) {
ListNode *Next = Curr->next;
Curr->next = Prev;
Prev = Curr;
Curr = Next;
}
return {Last, First};
}
public:
ListNode* reverseKGroup(ListNode* head, int k) {
// The likely most important function of Dummy is to return a (possibly
// changed) head easily. It's not just for iteration. See the two return
// statements below.
ListNode Dummy(0, head), *Prev = &Dummy;
while (head) {
ListNode *Last = Prev;
// Advance Last.
for (int i = 0; i < k; ++i) {
Last = Last->next;
if (!Last)
return Dummy.next;
}
// Keep in mind that if we have a Prev pointer, there's no need anymore
// to create another Curr pointer. Just Prev->next is enough.
ListNode *Next = Last->next;
std::tie(head, Last) = Reverse(head, Last);
// Connect the reversed list back to the original list.
Prev->next = head;
// Advance Prev and head for next iteration.
Prev = Last;
head = Next;
}
return Dummy.next;
}
};
用了头尾写法、正确的 Dummy 节点和 Prev 的特性之后你会发现一切都变得简单了。只能说牵涉到「翻转」的题目还是有点东西的,可能也是最复杂的,因为这牵涉到头的修改以及链表连接,也就是 Prev->next = head; 的逻辑。
三刷
class Solution {
ListNode *Reverse(ListNode *Head) {
ListNode *Prev = nullptr;
while (Head) {
ListNode *Next = Head->next;
Head->next = Prev;
Prev = Head;
Head = Next;
}
return Prev;
}
public:
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode *Curr = head;
ListNode Pre(0, head), *Prev = &Pre;
while (Curr) {
ListNode NextPre(0, Curr), *NextPrev = &NextPre;
for (int i = 0; i < k; ++i) {
NextPrev = NextPrev->next;
if (!NextPrev) {
Prev->next = Curr;
goto Finish;
}
}
ListNode *Next = NextPrev->next;
NextPrev->next = nullptr;
ListNode *RevCurr = Reverse(Curr);
// The difference between Pre and Prev are really subtle here! As you can
// see, the FIRST assignment of Prev->next does change the value of
// Pre.next, since at this time, Prev == &Pre.
Prev->next = RevCurr;
// However, when we advance Prev to Curr, even for once, Prev == &Pre NO
// LONGER holds. That's why we don't need complicated temporary variables
// to make sure that Pre.next points to our new head.
Prev = Curr;
Curr = Next;
}
Finish:
return Pre.next;
}
};
反转链表看起来终于是被我玩明白了?尤其是调用 Reverse() 的 call site 可以保留已经成为 tail 的 head,也即 Curr 这个特性非常重要。
此外,差不多也终于搞明白 Prev = &Pre 这个 idiom 有多强大了。上面注释里有写:第一次调用时 Prev == &Pre,此时对 Prev->next 赋值等价于对 Pre.next 赋值;然而,在后续的调用当中,由于 Prev != &Pre,所以 Pre.next 不再受影响。