在 O(n log n) 時間複雜度和常數級空間複雜度下,對鏈表進行排序。java
示例 1:node
輸入: 4->2->1->3 輸出: 1->2->3->4
示例 2:git
輸入: -1->5->3->4->0 輸出: -1->0->3->4->5
原題url:https://leetcode-cn.com/probl...github
題目很明確,排序,對於時間複雜度和空間複雜度有要求,針對O(n log n)
,讓我想到了歸併排序
和快速排序
,接下來咱們各自來看看。segmentfault
對了,這裏先統一放一下節點類,單向鏈表中的節點,存儲當前節點的值和後一個節點的引用。測試
Definition for singly-linked list. public class ListNode { int val; ListNode next; ListNode(int x) { val = x; } }
歸併排序,說白了,就是先分解到最小單元,而後逐個進行合併並排序,這樣在合併的時候,其實兩個鏈表自己就是有序的,那麼當有一個所有取完後,另外一個能夠直接拼接在最後面。優化
讓咱們看一下代碼:url
public class Solution { public ListNode sortList(ListNode head) { // 歸併排序 if (head == null || head.next == null) { return head; } // 先分隔,利用快慢指針分隔。 // 快指針先走,由於只有當空節點或1個節點纔是終止條件,2個節點的時候,若是不讓快指針先走,而是也指向head,那麼2個節點永遠不會被分隔,會陷入死循環 ListNode fast = head.next.next; ListNode slow = head; while (true) { if (fast == null || fast.next == null) { break; } fast = fast.next.next; slow = slow.next; } // 後半部分的開頭 ListNode second = slow.next; second = sortList(second); // 前半部分的開頭 slow.next = null; ListNode first = head; first = sortList(first); // 合併 ListNode result = new ListNode(0); head = result; while (first != null && second != null) { if (first.val < second.val) { result.next = first; first = first.next; } else { result.next = second; second = second.next; } result = result.next; } if (first != null) { result.next = first; } else { result.next = second; } return head.next; } }
提交OK,執行用時:5 ms
,內存消耗:39.6 MB
,執行用時只打敗了59.07%
的 java 提交記錄,應該還有優化的空間。spa
針對上面的代碼,在分隔的時候,設置fast = head.next.next
,這是由於咱們設置的遞歸終止條件是針對null
或者單個節點的。其實當只剩下兩個節點的時候,就能夠進行排序了,這樣應該能夠節省近一半的時間,固然了,從時間複雜度上來講並無改變。指針
咱們看一下代碼:
public class Solution { public ListNode sortList(ListNode head) { // 歸併排序 if (head == null || head.next == null) { return head; } // 說明只有兩個節點 if (head.next.next == null) { ListNode second = head.next; if (head.val > second.val) { return head; } else { second.next = head; head.next = null; return second; } } // 先分隔,利用快慢指針分隔。 ListNode fast = head; ListNode slow = head; while (true) { if (fast == null || fast.next == null) { break; } fast = fast.next.next; slow = slow.next; } // 後半部分的開頭 ListNode second = slow.next; second = sortList(second); // 前半部分的開頭 slow.next = null; ListNode first = head; first = sortList(first); // 合併 ListNode result = new ListNode(0); head = result; while (first != null && second != null) { if (first.val < second.val) { result.next = first; first = first.next; } else { result.next = second; second = second.next; } result = result.next; } if (first != null) { result.next = first; } else { result.next = second; } return head.next; } }
執行用時,有的時候是4 ms
,有的時候是3 ms
,看來歸併排序這條路差很少就是這樣了。
快速排序的思想就是選擇一個標準值,將比它大的和比它的小的,作交換。針對鏈表這種結構,就是將比它大的放在一個鏈表中,比它小的放在一個鏈表中,和它同樣大的,放在另外一個鏈表中。而後針對小的和大的鏈表,繼續排序。最終將三個鏈表按照小、相等、大進行鏈接。
接下來讓咱們看看代碼:
class Solution { public ListNode sortList(ListNode head) { // 利用快排 // 單個節點是終止節點 if (head == null || head.next == null) { return head; } // 比標準值小的節點 ListNode lowHead = new ListNode(0); ListNode low = lowHead; // 和標準值同樣的節點 ListNode midHead = new ListNode(0); ListNode mid = midHead; // 比標準值大的節點 ListNode highHead = new ListNode(0); ListNode high = highHead; // 標準值 int val = head.val; ListNode node = head; // 遍歷 while (node != null) { // 比標準值大的節點 if (node.val > val) { high.next = node; high = high.next; } // 比標準值小的節點 else if (node.val < val) { low.next = node; low = low.next; } // 和標準值同樣的節點 else { mid.next = node; mid = mid.next; } node = node.next; } // 終止,避免形成環 low.next = null; high.next = null; lowHead.next = sortList(lowHead.next); highHead.next = sortList(highHead.next); // 找出小節點鏈表的末尾 low = lowHead; while (low.next != null) { low = low.next; } // 拼接 low.next = midHead.next; mid.next = highHead.next; return lowHead.next; } }
提交OK,執行用時:2 ms
,內存消耗:40.01 MB
。
和歸併排序相比,時間更短,至於緣由,我確實是沒有想明白,由於都須要比較,而後從新構造新鏈表。我猜想是測試數據離散程度更高,這樣歸併排序的話,並無充分利用其特性:
當兩個鏈表合併時,若是一個鏈表已經所有結束,另外一個鏈表剩餘的部分能夠直接拼接。
以上就是這道題目個人解答過程了,不知道你們是否理解了。針對它的時間複雜度要求,利用歸併排序或者快速排序解決。
有興趣的話能夠訪問個人博客或者關注個人公衆號、頭條號,說不定會有意外的驚喜。
公衆號:健程之道