Sort List

Question

  1. Sort a linked list in O(n log n) time using constant space complexity.

题解1 - 归并排序(链表长度求中间节点)

链表的排序操作,对于常用的排序算法,能达到 O(n \log n)的复杂度有快速排序(平均情况),归并排序,堆排序。快速排序不一定能保证其时间复杂度一定满足要求,归并排序和堆排序都能满足复杂度的要求。在数组排序中,归并排序通常需要使用 O(n) 的额外空间,也有原地归并的实现,代码写起来略微麻烦一点。但是对于链表这种非随机访问数据结构,所谓的「排序」不过是指针next值的变化而已,主要通过指针操作,故仅需要常数级别的额外空间,满足题意。堆排序通常需要构建二叉树,在这道题中不太适合。

既然确定使用归并排序,我们就来思考归并排序实现的几个要素。

  1. 按长度等分链表,归并虽然不严格要求等分,但是等分能保证线性对数的时间复杂度。由于链表不能随机访问,故可以先对链表进行遍历求得其长度。
  2. 合并链表,细节已在 Merge Two Sorted Lists | Data Structure and Algorithm 中详述。

在按长度等分链表时进行「后序归并」——先求得左半部分链表的表头,再求得右半部分链表的表头,最后进行归并操作。

由于递归等分链表的操作需要传入链表长度信息,故需要另建一辅助函数。新鲜出炉的源码如下。

  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: You should return the head of the sorted linked list,
  18. using constant space complexity.
  19. */
  20. ListNode *sortList(ListNode *head) {
  21. if (NULL == head) {
  22. return NULL;
  23. }
  24. // get the length of List
  25. int len = 0;
  26. ListNode *node = head;
  27. while (NULL != node) {
  28. node = node->next;
  29. ++len;
  30. }
  31. return sortListHelper(head, len);
  32. }
  33. private:
  34. ListNode *sortListHelper(ListNode *head, const int length) {
  35. if ((NULL == head) || (0 >= length)) {
  36. return head;
  37. }
  38. ListNode *midNode = head;
  39. int count = 1;
  40. while (count < length / 2) {
  41. midNode = midNode->next;
  42. ++count;
  43. }
  44. ListNode *rList = sortListHelper(midNode->next, length - length / 2);
  45. midNode->next = NULL;
  46. ListNode *lList = sortListHelper(head, length / 2);
  47. return mergeList(lList, rList);
  48. }
  49. ListNode *mergeList(ListNode *l1, ListNode *l2) {
  50. ListNode *dummy = new ListNode(0);
  51. ListNode *lastNode = dummy;
  52. while ((NULL != l1) && (NULL != l2)) {
  53. if (l1->val < l2->val) {
  54. lastNode->next = l1;
  55. l1 = l1->next;
  56. } else {
  57. lastNode->next = l2;
  58. l2 = l2->next;
  59. }
  60. lastNode = lastNode->next;
  61. }
  62. lastNode->next = (NULL != l1) ? l1 : l2;
  63. return dummy->next;
  64. }
  65. };

源码分析

  1. 归并子程序没啥好说的了,见 Merge Two Sorted Lists | Data Structure and Algorithm.
  2. 在递归处理链表长度时,分析方法和 Convert Sorted List to Binary Search Tree | Data Structure and Algorithm 一致,count表示遍历到链表中间时表头指针需要移动的节点数。在纸上分析几个简单例子后即可确定,由于这个题需要的是「左右」而不是二叉搜索树那道题需要三分——「左中右」,故将count初始化为1更为方便,左半部分链表长度为length / 2, 这两个值的确定最好是先用纸笔分析再视情况取初值,不可死记硬背。
  3. 找到中间节点后首先将其作为右半部分链表处理,然后将其next值置为NULL, 否则归并子程序无法正确求解。这里需要注意的是midNode是左半部分的最后一个节点,midNode->next才是链表右半部分的起始节点。
  4. 递归模型中左、右、合并三者的顺序可以根据分治思想确定,即先找出左右链表,最后进行归并(因为归并排序的前提是两个子链表各自有序)。

复杂度分析

遍历求得链表长度,时间复杂度为 O(n), 「折半取中」过程中总共有 \log(n) 层,每层找中点需遍历 n/2 个节点,故总的时间复杂度为 n/2 \cdot O(\log n) (折半取中), 每一层归并排序的时间复杂度介于 O(n/2)O(n)之间,故总的时间复杂度为 O(n \log n), 空间复杂度为常数级别,满足题意。

题解2 - 归并排序(快慢指针求中间节点)

除了遍历链表求得总长外,还可使用看起来较为巧妙的技巧如「快慢指针」,快指针每次走两步,慢指针每次走一步,最后慢指针所指的节点即为中间节点。使用这种特技的关键之处在于如何正确确定快慢指针的起始位置。

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: You should return the head of the sorted linked list,
  18. using constant space complexity.
  19. */
  20. ListNode *sortList(ListNode *head) {
  21. if (NULL == head || NULL == head->next) {
  22. return head;
  23. }
  24. ListNode *midNode = findMiddle(head);
  25. ListNode *rList = sortList(midNode->next);
  26. midNode->next = NULL;
  27. ListNode *lList = sortList(head);
  28. return mergeList(lList, rList);
  29. }
  30. private:
  31. ListNode *findMiddle(ListNode *head) {
  32. if (NULL == head || NULL == head->next) {
  33. return head;
  34. }
  35. ListNode *slow = head, *fast = head->next;
  36. while(NULL != fast && NULL != fast->next) {
  37. fast = fast->next->next;
  38. slow = slow->next;
  39. }
  40. return slow;
  41. }
  42. ListNode *mergeList(ListNode *l1, ListNode *l2) {
  43. ListNode *dummy = new ListNode(0);
  44. ListNode *lastNode = dummy;
  45. while ((NULL != l1) && (NULL != l2)) {
  46. if (l1->val < l2->val) {
  47. lastNode->next = l1;
  48. l1 = l1->next;
  49. } else {
  50. lastNode->next = l2;
  51. l2 = l2->next;
  52. }
  53. lastNode = lastNode->next;
  54. }
  55. lastNode->next = (NULL != l1) ? l1 : l2;
  56. return dummy->next;
  57. }
  58. };

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: You should return the head of the sorted linked list,
  16. using constant space complexity.
  17. */
  18. public ListNode sortList(ListNode head) {
  19. // write your code here
  20. if (head == null || head.next == null) return head;
  21. ListNode mid = findMid(head);
  22. ListNode head1 = head;
  23. ListNode head2 = mid.next;
  24. mid.next = null;
  25. ListNode left = sortList(head1);
  26. ListNode right = sortList(head2);
  27. return merge(left, right);
  28. }
  29. // find mid
  30. public ListNode findMid(ListNode head) {
  31. if (head == null) return null;
  32. ListNode fast = head.next;
  33. ListNode slow = head;
  34. while (fast != null && fast.next != null) {
  35. fast = fast.next.next;
  36. slow = slow.next;
  37. }
  38. return slow;
  39. }
  40. // merge
  41. public ListNode merge(ListNode head1, ListNode head2) {
  42. ListNode dummy = new ListNode(0);
  43. ListNode head = dummy;
  44. while (head1 != null || head2 != null) {
  45. int a = head1 == null ? Integer.MAX_VALUE : head1.val;
  46. int b = head2 == null ? Integer.MAX_VALUE : head2.val;
  47. if (a < b) {
  48. head.next = new ListNode(a);
  49. if (head1 != null) head1 = head1.next;
  50. } else {
  51. head.next = new ListNode(b);
  52. if (head2 != null) head2 = head2.next;
  53. }
  54. head = head.next;
  55. }
  56. return dummy.next;
  57. }
  58. }

源码分析

  1. 异常处理不仅考虑了head, 还考虑了head->next, 可减少辅助程序中的异常处理。
  2. 使用快慢指针求中间节点时,将fast初始化为head->next可有效避免无法分割两个节点如1->2->null[^fast_slow_pointer]。
    • 求中点的子程序也可不做异常处理,但前提是主程序sortList中对head->next做了检测。
  3. 最后进行merge归并排序。

Note 在递归和迭代程序中,需要尤其注意终止条件的确定,以及循环语句中变量的自增,以防出现死循环或访问空指针。

复杂度分析

同上。

题解3 - 归并排序(自底向上)

归并排序,总的时间复杂度是(nlogn),但是递归的空间复杂度并不是常数(和递归的层数有着关;递归的过程是自顶向下,好理解;这里提供自底向上的非递归方法;

C++

  1. /**
  2. * Definition for singly-linked list.
  3. * struct ListNode {
  4. * int val;
  5. * ListNode *next;
  6. * ListNode(int x) : val(x), next(NULL) {}
  7. * };
  8. */
  9. class Solution {
  10. public:
  11. ListNode* sortList(ListNode* head) {
  12. int len_list = 0;
  13. ListNode *p=head;
  14. while(p){
  15. p = p->next;
  16. len_list++;
  17. }
  18. ListNode *l_list,*r_list,**p_merge_list;
  19. for(int i = 1; i < len_list; i <<= 1){
  20. r_list = l_list = head;
  21. p_merge_list = &head;
  22. for(int j = 0; j < len_list - i ; j += i << 1){
  23. for(int k = 0; k < i; ++k) r_list=r_list->next;
  24. int l_len=i,r_len=min(i, len_list - j - i);
  25. while(l_len || r_len ){
  26. if(r_len > 0 && (l_len == 0 || r_list->val <= l_list->val)){
  27. *p_merge_list = r_list;
  28. p_merge_list=&(r_list->next);
  29. r_list = r_list->next;
  30. --r_len;
  31. }
  32. else{
  33. *p_merge_list = l_list;
  34. p_merge_list=&(l_list->next);
  35. l_list = l_list->next;
  36. --l_len;
  37. }
  38. }
  39. l_list=r_list;
  40. }
  41. *p_merge_list = r_list;
  42. }
  43. return head;
  44. }
  45. };

复杂度分析

归并排序,分解子问题的过程是O(logn),合并子问题的过程是O(n);

Reference