LeetCode | 148. 排序链表
原题(Medium):
在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
思路:自底向上(bottom-to-up)、归并排序(Merge Sort)
题目对时间复杂度和空间复杂度做出了要求,常用的对数级别的排序方法不多,由于这里并不是双向链表,所以快排不太可能,可以使用归并,但一般的归并是需要一辅助数组的,空间复杂度为O(n),自然不能按一般归并的思路。我们可以考虑自底向上的归并,而非自上而下的递归归并,就是直接从两两合并开始,然后四四合并.......直到结束。
那么怎么把链表两两分开再两两合并呢?首先使用一整型变量size记录每轮需要两两分开和归并的节点数量,就是一个每次两倍倍增的数值(1、2、4....),我们可以使用位运算实现两倍倍增。然后怎么分开呢?要怎样把链表每两个节点分开成左右各一个,如stpe1一样?我们需要利用这个size把链表切开(step1的size为1),从链表头开始,走过size个节点(包括头节点)后,把链表断开,断开后左边就是左半部分,右边的size个就是右半部分,例如4->3->...,断开后就变成了4,3->....,此时4就是左半部分,3就是右半部分,怎么把右半部分在链表中确认下来?继续把链表切开,从3(右半部分的起点)出发,走过size个节点(包括出发的节点)后,把链表断开,此时左右部分都具备了,就可以归并了,至于两两归并的思路这里就不再赘述,因为如何两两分开才是难题,两两归并并不是。按这么说,我们需要一个切割函数:从一个指定的起点beg出发,走过n个节点后,把beg+n的那个节点下一节点返回,在此之前,先把beg+n的那个节点断后(先用临时变量记录next,再把next置空)。
我们可以总结一下我们的第一次两两归并做了什么
- 切割一次链表,得到左半部分和右半部分的起点
- 再切割一次链表,得到右半部分和下一次两两归并的左半部分的起点
- 归并左右部分
那么我们就可以根据第一次归并中得到的下一次归并的左半部分起点来继续第二次归并。直至到达链表尾端。
但这第一次归并做得还不够多,假设我们已经将左右部分归并好了,那么得到的结果自然就是3->4,显然问题就是它并没有重新连接回链表上,我们在切割函数上获得了下一次归并的起点,但我们不应该直接去连接这个起点,因为下一次归并还没有开始,至少我们需要等待下一次归并完成后才进行连接,所以我们需要一个指针变量记录该次归并后的最后一个节点,在例子里就是4这个结点,我们需要把它与下一次归并完成后的头节点进行连接。因此我们需要负责归并的函数返回归并后的头节点。
1 ListNode* sortList(ListNode* head) { 2 3 ListNode dummyHead(0); 4 dummyHead.next = head; 5 ListNode *temp = head; 6 int length = 0; 7 //计算链表的长度 8 while (temp) 9 { 10 length++; 11 temp = temp->next; 12 } 13 14 //不超出链表长度的情况下,每一轮归并数组的大小增大两倍 15 for (int size = 1; size < length; size <<= 1) 16 { 17 //负责记录归并后的尾节点,用于连接下一次归并 18 ListNode* tail = &dummyHead; 19 ListNode* cur = dummyHead.next; 20 while (cur) { 21 auto left = cur; 22 auto right = cut(left, size); //第一次切割,从左半部分的起点出发,获得左半部分和右半部分的起点 23 cur = cut(right, size); //第二次切割,从右半部分起点出发,获得右半部分和下一次归并的左半部分的起点(用cur记录) 24 tail->next = merge(left, right); //合并左右部分,并用上一次归并后的尾节点来连接这一次归并后的首节点 25 while (tail->next) tail = tail->next; //获得这一次归并后的尾节点 26 } 27 } 28 return dummyHead.next; 29 }
切割函数:从一个指定的起点beg出发,走过n个节点后,把beg+n的那个节点下一节点返回,在此之前,先把beg+n的那个节点断后(先用临时变量记录next,再把next置空)。
1 ListNode* cut(ListNode* begin, int size) { 2 while (--size && begin) begin = begin->next; //跨越包括自己的size个节点,所以是--size 3 4 if (!begin) return NULL; //如果循环结束begin为空,说明已经走到了链表的尽头,不存在下一部分的起点了,也不用断后了,已经无后了。 5 ListNode* next = begin->next; //记录下一部分的起点,用于返回 6 begin->next = NULL; //断后 7 return next; 8 }
合并函数:将左右两部分按顺序合并,并返回合并后的首节点。
1 ListNode* merge(ListNode* l, ListNode* r) { 2 ListNode dummyHead(0); 3 ListNode* temp = &dummyHead; 4 while (l&&r) 5 { 6 if (l->val>r->val) 7 { 8 temp->next = r; 9 temp = r; 10 r = r->next; 11 } 12 else 13 { 14 temp->next = l; 15 temp = l; 16 l = l->next; 17 } 18 } 19 temp->next = l ? l : r; 20 return dummyHead.next; 21 }