原题链接:https://oj.leetcode.com/problems/sort-list/
题目:空间复杂度为常数,时间复杂度为O(nlogn)的排序链表实现
方法一:第一想法是模拟数组的快速排序,参考了算法导论,于是思路被牵到了如何处理交换节点上,几经波折总算实现,不过提交的结果TLE。
1 /** 2 * Definition for partition method result value 3 * 4 */ 5 class PartitionResult { 6 ListNode head; 7 ListNode tail; 8 ListNode pre_pivot; 9 ListNode pivot_node; 10 11 public PartitionResult(ListNode head, ListNode tail) { 12 // TODO Auto-generated constructor stub 13 this.head = head; 14 this.tail = tail; 15 pre_pivot = null; 16 pivot_node = null; 17 } 18 } 19 20 public class Solution { 21 ListNode head = null; 22 23 private void swap(ListNode prei, ListNode i, ListNode prej, ListNode j) { 24 if (i != prej) { //i isn't adjacent to j 25 if (prei != null) //prei == null means i is the list's head 26 prei.next = j; 27 28 ListNode cpy = j.next; 29 j.next = i.next; 30 31 prej.next = i; 32 i.next = cpy; 33 } 34 else { //i adjacent to j means i == prej 35 if (prei != null) //prei == null means i is the list's head 36 prei.next = j; 37 38 ListNode cpy = j.next; 39 j.next = i; 40 i.next = cpy; 41 } 42 } 43 44 /** 45 * partition [p, r] inplace and return [head, tail, pre_pivot, pivot_node] 46 * 47 * @param preP 48 * @param p 49 * @param r 50 * @param nextR 51 * @return 52 */ 53 private PartitionResult partition(ListNode prep, ListNode p, ListNode r) { 54 int pivot_element = r.val; 55 56 PartitionResult partitionResult = new PartitionResult(p, r); 57 58 ListNode i = prep; 59 ListNode prei = null; 60 ListNode prej = prep; 61 62 for (ListNode j = p; j != r; prej = j, j = j.next) { 63 if (j.val <= pivot_element) { 64 65 prei = i; 66 67 //++i 68 if (i != null) { 69 i = i.next; 70 } 71 else { 72 i = partitionResult.head; 73 partitionResult.head = j; //modify cur head 74 75 if (this.head == i) 76 this.head = j; 77 } 78 79 //swap i node and j node 80 if (i != j) { 81 swap(prei, i, prej, j); 82 83 //swap i and j reference 84 ListNode cpy = i; 85 i = j; 86 j = cpy; 87 } 88 } 89 } 90 91 //swap i + 1 node and r node 92 if (i != null) { 93 prei = i; 94 i = i.next; 95 } 96 else { 97 i = partitionResult.head; 98 partitionResult.head = r; 99 100 if (this.head == i) 101 this.head = r; 102 } 103 104 swap(prei, i, prej, r); 105 106 ListNode cpy = i; 107 i = r; 108 r = cpy; 109 110 //modify tail 111 partitionResult.tail = i; 112 113 //set new pre pivot node and pivot node 114 partitionResult.pre_pivot = prej; 115 partitionResult.pivot_node = i; 116 117 return partitionResult; 118 } 119 120 121 /** 122 * single linked list quickSort [head, tail] 123 * @param head 124 * @param tail 125 * @return 126 */ 127 private void quickSort(ListNode preHead, ListNode head, ListNode tail) { 128 if (head != null && tail != null && head != tail) { 129 PartitionResult partitionResult = partition(preHead, head, tail); 130 131 quickSort(preHead, partitionResult.head, partitionResult.pre_pivot); 132 133 if (partitionResult.pivot_node != partitionResult.tail) 134 quickSort(partitionResult.pivot_node, partitionResult.pivot_node.next, partitionResult.tail); 135 } 136 } 137 138 public ListNode sortList(ListNode head) { 139 this.head = head; 140 ListNode tail = null; 141 142 for (ListNode itr = head; itr != null; tail = itr, itr = itr.next); 143 144 quickSort(null, head, tail); 145 146 return head; 147 } 148 }
方法一的缺点很明显:复杂容易出错,没有利用链表的优势。数组快排交换节点的本质,是使得在左边元素<=Pivot元素<=右边元素,因此在对链表进行快排时,可以构造左链表(l1),右链表(l2),及Pivot元素(x),使得l1 <= x < l2,再将l1 -> x -> l2相连,由此得到方法二。方法二的代码量较之方法一减少一半,无奈提交的结果仍然是TLE,错误的case与方法一一致,都是一个超长的输入。
public class Solution { /** * core idea is the link not swap * head != null * and use the head node as a pivot node * * [head, tail) * * @param head * @param tail * @return current head node */ private ListNode partition(ListNode head, ListNode tail) { int x = head.val; //l1 <= x //l2 > x ListNode l1Head = new ListNode(-1), l1Itr = l1Head; ListNode l2Head = new ListNode(-1), l2Itr = l2Head; for (ListNode itr = head.next; itr != tail; itr = itr.next) { if (itr.val <= x) { l1Itr.next = itr; l1Itr = itr; } else { l2Itr.next = itr; l2Itr = itr; } } //l1->x->l2->tail l1Itr.next = head; l2Itr.next = tail; //if l2Head == l2Itr head.next = l2Head.next; //useless node set to null ListNode relHead = l1Head.next; l1Head = null; l2Head = null; return relHead; } //quick sort for list private ListNode quickSort(ListNode head, ListNode tail) { ListNode curHead = head; if (head != tail) { curHead = partition(head, tail); //after partition head node play a pivot role curHead = quickSort(curHead, head); //maintain head node head.next = quickSort(head.next, tail); //link two parts } return curHead; } public ListNode sortList(ListNode head) { return quickSort(head, null); } }
影响快排性能的一个重要因素,就是Pivot元素的选取。方法二简单的使用了链表中的第一个节点作为Pivot元素,并不能保证很好的平均性能。参考了Discuss中一位网友取链表均值作为Pivot值的思路,实现了方法三。注意,之所以说是Pivot值,是因为与方法一,方法二在链表中选取Pivot元素不同,该Pivot值可能不在链表中。
1 public class Solution { 2 /** 3 * core idea is the link not swap 4 * head != null 5 * and use the head node as a pivot node 6 * 7 * [head, tail) 8 * 9 * @param head 10 * @param tail 11 * @return [leftPartHead, leftPartEndNode] 12 */ 13 private ListNode[] partition(ListNode head, ListNode tail) { 14 //cal avg as the pivot value 15 int sum = 0, count = 0; 16 17 for (ListNode itr = head; itr != tail; itr = itr.next) { 18 sum += itr.val; 19 ++count; 20 } 21 22 float x = (float)sum / count; //notice if int x will lead to infinite loop (for example -39 -38) 23 24 boolean same = true; 25 26 //l1 <= x 27 //l2 > x 28 ListNode l1Head = new ListNode(-1), l1Itr = l1Head; 29 ListNode l2Head = new ListNode(-1), l2Itr = l2Head; 30 31 for (ListNode itr = head, pre = head; itr != tail; pre = itr, itr = itr.next) { 32 if (itr.val != pre.val) { 33 same = false; 34 } 35 36 if (itr.val < x) { 37 l1Itr.next = itr; 38 l1Itr = itr; 39 } 40 else { 41 l2Itr.next = itr; 42 l2Itr = itr; 43 } 44 } 45 46 ListNode [] listNodes = new ListNode[2]; 47 48 listNodes[0] = l1Head.next; 49 50 if (!same) { 51 //l1->l2->tail 52 l2Itr.next = tail; //if l2Head == l2Itr 53 l1Itr.next = l2Head.next; 54 55 listNodes[1] = l1Itr; 56 } 57 else { 58 listNodes[1] = l1Head.next; 59 } 60 61 //useless node set to null 62 l1Head = null; 63 l2Head = null; 64 65 return listNodes; 66 } 67 68 //quick sort for list 69 private ListNode quickSort(ListNode head, ListNode tail) { 70 ListNode curHead = head; 71 72 if (head != tail && head.next != tail) { 73 ListNode [] rel = partition(head, tail); //after partition head node play a pivot role 74 75 if (rel[0] != null) { //when rel[0] means that remain element is the same 76 curHead = quickSort(rel[0], rel[1].next); //maintain head node 77 78 rel[1].next = quickSort(rel[1].next, tail); //link the two parts 79 } 80 } 81 82 return curHead; 83 } 84 85 public ListNode sortList(ListNode head) { 86 return quickSort(head, null); 87 } 88 }
方法三的trap:
1. 由于采用均值作为Pivot值,因此当链表中元素相等时,是没法继续划分的(当然也不需要继续划分,即可以结束),会造成无限循环
2. 题目中给的链表元素值为int型,如果均值为int,也会造成无法继续划分的情况,如{5, 6},均值为5,那么5, 6将被归为右链表,并且那么持续下去,造成无限循环
方法三提交结果总算AC啦(512ms),时间有波动。
方法四不再死磕链表快排,采用归并排序,对于此题来说,应该是比较直接合理的解决方案。值得注意的是怎么确定链表的中点(经典问题啦):中点意味着2*mid = length,因此可以设置两个引用,mid引用一次走一个节点,itr引用一次走两个节点,当itr到链表尾的时候,mid就在近似链表中间的位置了。之所以说是近似呢,是因为在我的代码中,mid和itr都是从链表中的第一个节点开始遍历的,因此length相当于-1了,对于奇数节点来说,mid为实际中间节点+1,偶数节点为中间节点右边那个节点。
1 public class Solution { 2 /** 3 * merge l1 and l2 list 4 * 5 * @param l1 6 * @param l2 7 * @return 8 */ 9 private ListNode merge(ListNode l1, ListNode l2) { 10 ListNode head = new ListNode(-1), itr = head; 11 12 while (l1 != null && l2 != null) { 13 if (l1.val < l2.val) { 14 itr.next = l1; 15 itr = l1; 16 l1 = l1.next; 17 } 18 else { 19 itr.next = l2; 20 itr = l2; 21 l2 = l2.next; 22 } 23 } 24 25 //deal l1 or l2 remain element 26 ListNode remail = null; 27 28 if (l1 == null && l2 != null) { 29 remail = l2; 30 } 31 else if (l2 == null && l1 != null) { 32 remail = l1; 33 } 34 35 itr.next = remail; 36 37 ListNode relHead = head.next; 38 head = null; 39 40 return relHead; 41 } 42 43 private ListNode mergeSort(ListNode head, ListNode tail) { 44 if (head.next == tail) { //single node 45 head.next = null; 46 return head; 47 } 48 49 //locate the middle node 50 //2 * mid = len 51 //itr += 2; mid += 1; 52 //notice that itr and mid start from the first node so it is not a exact middle location 53 //actually it is the middle location + 1 54 ListNode itr = head, mid = head; 55 while (itr != tail) { 56 itr = itr.next; 57 58 if (itr != tail) { 59 itr = itr.next; 60 } 61 62 mid = mid.next; 63 } 64 65 ListNode l1 = mergeSort(head, mid); 66 67 ListNode l2 = mergeSort(mid, tail); 68 69 return merge(l1, l2); 70 } 71 72 public ListNode sortList(ListNode head) { 73 if (head == null) //trap 74 return null; 75 76 return mergeSort(head, null); 77 } 78 }
方法四提交结果AC(500ms),时间有波动。
github地址:https://github.com/zrss/leetcode/tree/master/src/com/zrss/leetcode