25. K 个一组翻转链表

25. K 个一组翻转链表 - 力扣(LeetCode)

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 不再受影响。