Reorder List

Question

Problem Statement

Given a singly linked list L: L_0→_L_1→…→_L__n-1→L_n,
reorder it to: _L_0→_L__n
L_1→_L__n-1→L_2→_L__n-2→…

You must do this in-place without altering the nodes’ values.

For example,
Given {1,2,3,4}, reorder it to {1,4,2,3}.

" class="reference-link">题解1 - 链表长度(TLE)

直观角度来考虑,如果把链表视为数组来处理,那么我们要做的就是依次将下标之和为n的两个节点链接到一块儿,使用两个索引即可解决问题,一个索引指向i, 另一个索引则指向其之后的第n - 2*i个节点(对于链表来说实际上需要获取的是其前一个节点), 直至第一个索引大于第二个索引为止即处理完毕。

既然依赖链表长度信息,那么要做的第一件事就是遍历当前链表获得其长度喽。获得长度后即对链表进行遍历,小心处理链表节点的断开及链接。用这种方法会提示 TLE,也就是说还存在较大的优化空间!

C++ - TLE

  1. /**
  2. * Definition of ListNode
  3. * class ListNode {
  4. * public:
  5. * int val;
  6. * ListNode *next;
  7. * ListNode(int val) {
  8. * this->val = val;
  9. * this->next = NULL;
  10. * }
  11. * }
  12. */
  13. class Solution {
  14. public:
  15. /**
  16. * @param head: The first node of linked list.
  17. * @return: void
  18. */
  19. void reorderList(ListNode *head) {
  20. if (NULL == head || NULL == head->next || NULL == head->next->next) {
  21. return;
  22. }
  23. ListNode *last = head;
  24. int length = 0;
  25. while (NULL != last) {
  26. last = last->next;
  27. ++length;
  28. }
  29. last = head;
  30. for (int i = 1; i < length - i; ++i) {
  31. ListNode *beforeTail = last;
  32. for (int j = i; j < length - i; ++j) {
  33. beforeTail = beforeTail->next;
  34. }
  35. ListNode *temp = last->next;
  36. last->next = beforeTail->next;
  37. last->next->next = temp;
  38. beforeTail->next = NULL;
  39. last = temp;
  40. }
  41. }
  42. };

源码分析

  1. 异常处理,对于节点数目在两个以内的无需处理。
  2. 遍历求得链表长度。
  3. 遍历链表,第一个索引处的节点使用last表示,第二个索引处的节点的前一个节点使用beforeTail表示。
  4. 处理链表的链接与断开,迭代处理下一个last

复杂度分析

  1. 遍历整个链表获得其长度,时间复杂度为 $$O(n)$$.
  2. 双重for循环的时间复杂度为 $$(n-2) + (n-4) + … + 2 = O(\frac{1}{2} \cdot n^2)$$.
  3. 总的时间复杂度可近似认为是 $$O(n^2)$$, 空间复杂度为常数。

Warning 使用这种方法务必注意ij的终止条件,若取i < length + 1 - i, 则在处理最后两个节点时会出现环,且尾节点会被删掉。在对节点进行遍历时务必注意保留头节点的信息!

题解2 - 反转链表后归并

既然题解1存在较大的优化空间,那我们该从哪一点出发进行优化呢?擒贼先擒王,题解1中时间复杂度最高的地方在于双重for循环,在对第二个索引进行遍历时,j每次都从i处开始遍历,要是j能从链表尾部往前遍历该有多好啊!这样就能大大降低时间复杂度了,可惜本题的链表只是单向链表… 有什么特技可以在单向链表中进行反向遍历吗?还真有——反转链表!一语惊醒梦中人。

C++

  1. /**
  2. * Definition of ListNode
  3. * class ListNode {
  4. * public:
  5. * int val;
  6. * ListNode *next;
  7. * ListNode(int val) {
  8. * this->val = val;
  9. * this->next = NULL;
  10. * }
  11. * }
  12. */
  13. class Solution {
  14. public:
  15. /**
  16. * @param head: The first node of linked list.
  17. * @return: void
  18. */
  19. void reorderList(ListNode *head) {
  20. if (head == NULL || head->next == NULL) return;
  21. // find middle
  22. ListNode *slow = head, *fast = head->next;
  23. while (fast != NULL && fast->next != NULL) {
  24. slow = slow->next;
  25. fast = fast->next->next;
  26. }
  27. ListNode *rHead = slow->next;
  28. slow->next = NULL;
  29. // reverse ListNode on the right side
  30. ListNode *prev = NULL;
  31. while (rHead != NULL) {
  32. ListNode *temp = rHead->next;
  33. rHead->next = prev;
  34. prev = rHead;
  35. rHead = temp;
  36. }
  37. // merge two list
  38. rHead = prev;
  39. ListNode *lHead = head;
  40. while (lHead != NULL && rHead != NULL) {
  41. ListNode *temp1 = lHead->next;
  42. lHead->next = rHead;
  43. ListNode *temp2 = rHead->next;
  44. rHead->next = temp1;
  45. lHead = temp1;
  46. rHead = temp2;
  47. }
  48. }
  49. };

Java

  1. /**
  2. * Definition for ListNode.
  3. * public class ListNode {
  4. * int val;
  5. * ListNode next;
  6. * ListNode(int val) {
  7. * this.val = val;
  8. * this.next = null;
  9. * }
  10. * }
  11. */
  12. public class Solution {
  13. /**
  14. * @param head: The head of linked list.
  15. * @return: void
  16. */
  17. public void reorderList(ListNode head) {
  18. if (head == null || head.next == null) return;
  19. // find middle
  20. ListNode slow = head, fast = head.next;
  21. while (fast != null && fast.next != null) {
  22. slow = slow.next;
  23. fast = fast.next.next;
  24. }
  25. ListNode rHead = slow.next;
  26. slow.next = null;
  27. // reverse ListNode on the right side
  28. ListNode prev = null;
  29. while (rHead != null) {
  30. ListNode temp = rHead.next;
  31. rHead.next = prev;
  32. prev = rHead;
  33. rHead = temp;
  34. }
  35. // merge two list
  36. rHead = prev;
  37. ListNode lHead = head;
  38. while (lHead != null && rHead != null) {
  39. ListNode temp1 = lHead.next;
  40. lHead.next = rHead;
  41. rHead = rHead.next;
  42. lHead.next.next = temp1;
  43. lHead = temp1;
  44. }
  45. }
  46. }

源码分析

相对于题解1,题解2更多地利用了链表的常用操作如反转、找中点、合并。

  1. 找中点:我在九章算法模板的基础上增加了对head->next的异常检测,增强了鲁棒性。
  2. 反转:非常精炼的模板,记牢!
  3. 合并:也可使用九章提供的模板,思想是一样的,需要注意left, rightdummy三者的赋值顺序,不能更改任何一步。

复杂度分析

找中点一次,时间复杂度近似为 O(n). 反转链表一次,时间复杂度近似为 O(n/2). 合并左右链表一次,时间复杂度近似为 O(n/2). 故总的时间复杂度为 O(n).

Reference