首页 > 技术文章 > leetcode: sortlist之四种方法

hzhesi 2014-10-29 10:11 原文

原题链接: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 }
View Code

方法一的缺点很明显:复杂容易出错,没有利用链表的优势。数组快排交换节点的本质,是使得在左边元素<=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);
    }
}
View Code

影响快排性能的一个重要因素,就是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 }
View Code

方法三的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 }
View Code

方法四提交结果AC(500ms),时间有波动。

github地址:https://github.com/zrss/leetcode/tree/master/src/com/zrss/leetcode

 

推荐阅读