在進入本節的正題以前,咱們先來看一道開胃菜。java
將兩個升序鏈表合併爲一個新的 升序 鏈表並返回。新鏈表是經過拼接給定的兩個鏈表的全部節點組成的。 node
示例:git
輸入:1->2->4, 1->3->4 輸出:1->1->2->3->4->4
直接兩個列表合併,排序,而後從新構建一個新的鏈表。github
public ListNode mergeTwoLists(ListNode l1, ListNode l2) { List<Integer> numsOne = getIntegerList(l1); List<Integer> numsTwo = getIntegerList(l2); numsOne.addAll(numsTwo); Collections.sort(numsOne); // 構建結果 return buildHead(numsOne); } private List<Integer> getIntegerList(ListNode oneNode) { // 使用 linkedList,避免擴容 List<Integer> resultList = new LinkedList<>(); while (oneNode != null) { int value = oneNode.val; resultList.add(value); oneNode = oneNode.next; } return resultList; } private ListNode buildHead(List<Integer> integers) { if(integers.size() == 0) { return null; } ListNode head = new ListNode(integers.get(0)); ListNode temp = head; for(int i = 1; i < integers.size(); i++) { temp.next = new ListNode(integers.get(i)); temp = temp.next; } return head; }
Runtime: 4 ms, faster than 22.43% of Java online submissions for Merge Two Sorted Lists. Memory Usage: 39.6 MB, less than 19.99% of Java online submissions for Merge Two Sorted Lists.
這種思路雖然簡單粗暴,可是效果確實不怎麼樣。算法
那麼如何改進呢?數組
主要的問題仍是出在列表原本就是有序的,咱們沒有很好的利用這個特性。less
直接循環一遍,對比兩者的數據大小,充分利用數組有序的特性。ide
public ListNode mergeTwoLists(ListNode l1, ListNode l2) { if(l1 == null) { return l2; } if(l2 == null) { return l1; } // 臨時變量 ListNode newNode = new ListNode(0); // 新增的頭指針 ListNode head = newNode; // 循環處理 while (l1 != null && l2 != null) { int valOne = l1.val; int valTwo = l2.val; // 插入小的元素節點 if(valOne <= valTwo) { newNode.next = l1; l1 = l1.next; } else { newNode.next = l2; l2 = l2.next; } // 變換 newNode newNode = newNode.next; } // 若是長度不同 if(l1 != null) { newNode.next = l1; } if(l2 != null) { newNode.next = l2; } return head.next; }
Runtime: 0 ms, faster than 100.00% of Java online submissions for Merge Two Sorted Lists. Memory Usage: 38.8 MB, less than 88.76% of Java online submissions for Merge Two Sorted Lists.
超過 100% 的提交者,此次還算比較滿意。優化
解決了這道開胃菜以後,讓咱們一塊兒看下後面的正菜。ui
- 合併K個排序鏈表
合併 k 個排序鏈表,返回合併後的排序鏈表。請分析和描述算法的複雜度。
示例:
輸入: [ 1->4->5, 1->3->4, 2->6 ] 輸出: 1->1->2->3->4->4->5->6
咱們按照和 2 個數組相似的策略,所有放在一個列表中,而後排序,最後構建。
代碼很是的簡單,以下:
public ListNode mergeKLists(ListNode[] lists) { if(null == lists || lists.length == 0) { return null; } // 查找操做比較少 List<Integer> integerList = new LinkedList<>(); for(ListNode listNode : lists) { integerList.addAll(getIntegerList(listNode)); } // 排序 Collections.sort(integerList); // 構建結果 return buildHead(integerList); } private List<Integer> getIntegerList(ListNode oneNode) { // 使用 linkedList,避免擴容 List<Integer> resultList = new LinkedList<>(); while (oneNode != null) { int value = oneNode.val; resultList.add(value); oneNode = oneNode.next; } return resultList; } private ListNode buildHead(List<Integer> integers) { if(integers.size() == 0) { return null; } ListNode head = new ListNode(integers.get(0)); ListNode temp = head; for(int i = 1; i < integers.size(); i++) { temp.next = new ListNode(integers.get(i)); temp = temp.next; } return head; }
Runtime: 103 ms, faster than 15.12% of Java online submissions for Merge k Sorted Lists. Memory Usage: 40.6 MB, less than 94.79% of Java online submissions for Merge k Sorted Lists.
花了共計 100ms,若是我說咱們的最終版本能夠把這個解法提高 100 倍,你信嗎?
這個問題和之前同樣,那咱們換一種套路。
實際上 n 個列表合併,咱們能夠拆分爲兩兩合併,最後變爲一個完整的鏈表。
public ListNode mergeKLists(ListNode[] lists) { if(null == lists || lists.length == 0) { return null; } // ListNode result = lists[0]; // 從第二個開始遍歷 for(int i = 1; i < lists.length; i++) { ListNode node = lists[i]; result = mergeTwoLists(result, node); } return result; }
mergeTwoLists 使咱們在兩個鏈表合併中的最佳解法,這裏複用了一下。
Runtime: 98 ms, faster than 16.31% of Java online submissions for Merge k Sorted Lists. Memory Usage: 41.4 MB, less than 47.50% of Java online submissions for Merge k Sorted Lists.
改善效果不是很明顯。
那麼如何改進呢?
實際上這裏有個問題就是咱們是依次遍歷,(1,2)合併成一個節點,和(3)繼續合併。
下面咱們來看一個比較取巧的解法。
咱們能夠藉助優先級隊列,讓咱們的排序從原來的 N 優化爲 LogN 。
public ListNode mergeKLists(ListNode[] lists) { if(null == lists || lists.length == 0) { return null; } PriorityQueue<ListNode> queue = new PriorityQueue<>(lists.length, new Comparator<ListNode>() { @Override public int compare(ListNode o1, ListNode o2) { return o1.val - o2.val; } }); // 循環添加元素 for(ListNode listNode : lists) { if(listNode != null) { queue.offer(listNode); } } // 依次彈出 return buildHead(queue); } /** * 構建頭節點 * @param queue 列表 * @return 結果 * @since v2 */ private ListNode buildHead(Queue<ListNode> queue) { ListNode dummy = new ListNode(0); ListNode tail = dummy; while (!queue.isEmpty()) { tail.next = queue.poll(); tail = tail.next; // 這裏相似於將 queue 層層剝開放入 queue 中 if(tail.next != null) { queue.add(tail.next); } } return dummy.next; }
Runtime: 4 ms, faster than 81.55% of Java online submissions for Merge k Sorted Lists. Memory Usage: 41.1 MB, less than 74.81% of Java online submissions for Merge k Sorted Lists.
4ms! 此次簡直是質的飛躍,從 100ms 提高了 25 倍左右。可喜可賀。
那麼,咱們會止步於此嗎?
還可以更上一層樓嗎?
這種 k 個有序鏈表的問題,其實均可以拆分爲更小的子問題。
全部相似的問題,基本上均可以使用 DP 或者分治的方式來解決。
本次展現一下分治算法,將合併的鏈表從中間拆分爲二個部分處理。
public ListNode mergeKLists(ListNode[] lists) { final int length = lists.length; if(lists.length == 0) { return null; } if(lists.length == 1) { return lists[0]; } // 遞歸獲取兩個節點 int mid = (length) / 2; ListNode one = mergeKLists(subArray(lists, 0, mid)); ListNode two = mergeKLists(subArray(lists, mid, length)); // 合併最後2個節點 return mergeTwoLists(one, two); } private ListNode[] subArray(ListNode[] listNodes, int start, int end) { int size = end-start; ListNode[] result = new ListNode[size]; int index = 0; for(int i = start; i < end; i++) { result[index++] = listNodes[i]; } return result; }
Runtime: 2 ms, faster than 91.66% of Java online submissions for Merge k Sorted Lists. Memory Usage: 41.5 MB, less than 34.83% of Java online submissions for Merge k Sorted Lists.
2ms! 咱們又把速度提高了一倍,這下你滿意了嗎?
無論你滿不滿意,我不滿意,由於還沒作到最好。
最明顯的一個地方就是咱們爲了使用分治,對數組進行復制拷貝,這種複製其實是很消耗時間的,那麼又沒有辦法能夠解決呢?
咱們分治是把數組分爲左右兩個部分,實際上咱們有另外一種辦法也能夠達到相似的效果。
好比:
[1, 2, 3, 4]
咱們能夠首位結合:
[(1,4), 2, 3] [(1,4,3), 2] [(1,4,3,2)]
這樣能夠達到一樣的效果,也避免了空間的浪費,和時間的消耗。
public ListNode mergeKLists(ListNode[] lists) { if (lists.length == 0) { return null; } int i = 0; int j = lists.length - 1; while (j > 0) { // ? i = 0; while (i < j) { lists[i] = mergeTwoLists(lists[i], lists[j]); i++; j--; } } return lists[0]; }
Runtime: 1 ms, faster than 100.00% of Java online submissions for Merge k Sorted Lists. Memory Usage: 41.5 MB, less than 35.98% of Java online submissions for Merge k Sorted Lists.
1ms! 咱們將這個算法從 100ms 優化到 1ms。
可見有時候 cpu 核數翻倍,也沒有一個優秀的算法來的效果顯著,這也正是算法的威力。
夜已經深了,本次解析先到這裏,後續將深刻講解一下本文提到的優先級隊列。
若是你對這個算法不滿意,在保住頭髮的前提下,請繼續優化~
https://leetcode.com/problems/merge-k-sorted-lists/submissions/
https://leetcode-cn.com/problems/merge-two-sorted-lists