1.Sort List
一、题目描述
Sort a linked list in O(n log n) time using constant space complexity.
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: ListNode *sortList(ListNode *head) { } };
二、思考过程
根据题目描述和前导代码可知:
- 链表为单向链表
- 时间复杂度为O(nlogn)
- 空间复杂度为O(1)
由时间复杂度为O(nlogn)可以想到快速排序、堆排序以及归并排序等,而快速排序和堆排序都不适合使用单向链表,所以想到使用归并排序。而采用归并排序时又有递归和非递归两种方法。由于题目要求解决方案必须具有常量的空间复杂度,所以递归的方法显然不行。
因而,最终决定采用非递归的归并排序方法解决该问题。
因为一开始我只了解归并排序的方法,所以我是从递归的归并排序算法开始思考具体解决过程的。首先,归并排序的递归算法的伪码如下:
merge_sort(datas, begin, end) { if(begin == end) return; mid = (begin + end)/2; merge_sort(datas, begin, mid); merge_sort(datas, mid + 1, end); merge(datas, begin, mid, end); } merge(datas, begin, mid, end) { create tmpDatas as large as datas in size; postBegin = mid + 1; tmpIndex = 0; while(begin <= mid && postBegin <= end){ if(datas[begin] < datas[postBegin]){ tmpDatas[tmpIndex++] = datas[begin++]; }else{ tmpDatas[tmpIndex++] = datas[postBegin++]; } } while(begin <= mid){ tmpDatas[tmpIndex++] = datas[begin++]; } while(postBegin <= end){ tmpDatas[tmpIndex++] = datas[postBegin++]; } copy tmpDatas to datas from the position begin in datas; }
总的思想是将要排序的较长的序列从中部分为较小的序列分别对其排序,然后再将排序好的两个子序列进行归并,以获得好的效率.而拆分的终点是序列的长度为1.因而可知,递归方式的归并排序其时间复杂度是O(nlogn),而空间复杂度则为是O(logn),不是常数.
并且可以发现,归并排序递归算法的设计是自顶往下的,而实际的程序执行则是自底往上的.所以,归并排序的非递归算法在设计和实现方面都应该是自底往上的.进一步分析上述递归过程,实际排序中其实是首先合并具有一个元素的子序列(因而是有序的),将相邻的含有一个元素的子序列merge为具有两个元素的有序子序列,然后再将相邻的含有两个元素的子序列merge为具有四个元素的有序子序列,依次类推,直到达到整个序列的长度.
所以,在非递归算法实现中,我们可以定义一个称为”步进”的概念,它表示当前要合并的子序列的长度.其大小初始化为1,表示首先合并长度为1的子序列,然后2倍的递增,直至达到整个序列的长度.
三、代码实现
实际实现时,主要任务集中在相邻子序列的合并上.结合LeetCode所给的前导代码,可以有如下一种实现:(个人观点,如有更好方法,非常希望能够分享)
ListNode *sortList(ListNode *head){ int ListLen = 0; int paceLen = 1; ListNode *th, *s1, *s2, *e1, *e2;//th always points to he node just be arranged //First, get the length of the list ListNode *tPtr = head; while(tPtr != NULL){ ListLen++; tPtr = tPtr -> next; } //Then, merge the two adjacent units in spaceLen while(paceLen < ListLen){ th = NULL; s1 = head; e1 = s1; while(e1 != NULL){//not arrive at the end of the list //init s1,s2,e1,e2; int i = 0; while(i < paceLen && e1 != NULL){ e1 = e1 -> next; i++; } if(e1 == NULL){//arrive at the end of the list th -> next = s1; break; } s2 = e1; e2 = s2; i = 0; while(i < paceLen && e2 != NULL){ e2 = e2 -> next; i++; } //init th and head if(s1 -> val < s2 -> val){ if(th != NULL){ th -> next = s1; }else{ head = s1; } th = s1; s1 = s1 ->next; }else{ if(th != NULL){ th -> next = s2; }else{ head = s2; } th = s2; s2 = s2 -> next; } //merge two units while(s1 != e1 && s2 != e2){ if(s1 -> val <= s2 -> val){ th -> next = s1; th = s1; s1 = s1 -> next; }else{ th ->next = s2; th = s2; s2 = s2 -> next; } } while(s1 != e1){ th -> next = s1; th = s1; s1 = s1 -> next; } while(s2 != e2){ th -> next = s2; th = s2; s2 = s2 -> next; } s1 = e2; e1 = s1;
th -> next = s1;//!!! } paceLen *= 2; } return head; }
虽然代码比较长,但内部while循环的复杂度为O(n).
代码说明:
- th始终指向最近被放置到当前来说正确位置的元素。当一个“步进”第一次应用时,th初始化为NULL;
- s1、s2分别指向将要被合并的相邻子序列的第一个元素,e1、e2分别指向将要被合并的相邻子序列的最后元素的后一个元素。e1、s2、e2都有可能为NULL;
- 程序的最开始,首先初始化“步进”paceLen为1,然后计算List的长度ListLen;
- 然后,在每一个“步进”的初始时,设置th、s1、e1;
- 在每一次合并时都要首先设置好s1、e1、s2和e2的值;
- 由于,在每一个“步进”的第一应用时都有可能改变第一个位置的元素,所以在第一应用时就要分析是否改变,如果改变就要记录。方法是,利用th是否为NULL来判断是否为第一次应用,如果是则分析;
- 在同一个”步进“的每次应用结束时,都要设置th所指元素的下一个元素是什么,这主要是为了设置每个”步进“的最后应用时最后一个元素的next指向为NULL。

浙公网安备 33010602011771号