1. 程式人生 > >Leetcode:Merge k Sorted Lists

Leetcode:Merge k Sorted Lists

遞歸函數 行合並 rec 分治算法 || close 保存 ... 歸納

題目大意是傳入一個鏈表數組lists,每個鏈表都由若幹個鏈接的鏈表結點組成,並且每個鏈表結點記錄一個整數。題目保證傳入的鏈表中的整數按從小到大進行排序。

題目要求我們輸出一個新的鏈表,這個鏈表中應該包含所有在lists中出現的整數,並且按從小到大排序。


我的思路:

這個問題解法應該很多。我采用的解法是分治算法,靈感來自於歸並排序,因為歸並排序的遞歸完成後也會涉及到將兩個有序數組重組合並。

首先說明如何重組兩個鏈表為一個有序鏈表,其思路源自於我們在玩撲克牌時對兩堆有序撲克牌進行排序的手法:

1.我們從兩疊有序撲克牌(正面向上)中,選取最小的牌面(即兩疊撲克牌蓋在最上面的兩張牌中較小的牌),並加入到手牌中。

2.循環上一步直到有一疊牌被完全取走。

3.將剩下的一疊牌全部按序加入到手牌中。

由於始終選取較小的牌,以及兩疊牌的從小到大排序的性質,能保證剩余的牌總是會不小於被選走的手牌。假設兩疊牌共n張,由於這個過程中每次都會使得桌上的牌減少1,而減少到0時就會必定結束,因此上面的步驟最多只會執行n次,這n次中每一次都涉及一次比較和取牌(在計算機中是常量時間),因此我們可以認為要合並兩個有序牌堆的時間復雜度為O(n)。

combine(A, B):

  ai = 0, bi = 0

  al = A.length, bl = B.length

  result = empty-list

  while(ai < al && bi < bl)

    if(A[ai] <= B[bi])

      insert A[ai] into result

      ai = ai + 1

    else

      insert B[bi] into result

      bi = bi + 1

  while(ai < al)

    insert A[ai] into result

    ai = ai + 1

  while(bi < bl)

    insert B[bi] into result

    bi = bi + 1

  return result

但是題目已經指出了,k:=lists.length不一定為2。因此我們要通過分治算法將需要合並的鏈表數減少為2。其思路是先合並下標為0~k/2的鏈表,在合並k/2~k的鏈表。最後利用我們上面所說的洗牌算法將兩個鏈表合並。

rec(lists, from, to) //合並lists[from], ... lists[to - 1]為一個鏈表,並返回合並的鏈表

  if(from + 1 == to)

    return lists[i]

  half = (from + to) / 2

  part1 = rec(lists, from, half)

  part2 = rec(lists, half, to)

  return combine(part1, part2)

上面就是我們歸並算法的完整實現。先說明它能返回正確的結果。當to - from == 1時,此時需要排序的鏈表只有一個,只要直接返回這個鏈表就可以了。因此當to - from == 1時這個算法是正確的。假設當to-from<p時(p >1),這個算法能返回正確的結果。那麽當to-from=p時,函數將排序lists中from~half段和half~to段的鏈表委托給遞歸函數。由於from < half < to,因此half-from<to-from=p,to-half<to-from=p,因此遞歸函數對於部分的排序返回了正確的結果,分別記為part1,part2,part1中保存了from~half段的合並結果,而part2中保存了half~to段的合並結果。最終利用我們已經說明過的洗牌合並法對合並part1和part2,因此最終得到的結果則是lists[from], ... lists[to - 1]的有序合並結果。這裏用了一個關鍵的想法,即合並順序不會改變最終結果。利用數學歸納法我們可以得出對於任意to-from>=1,rec函數都能返回正確的結果。

接下來說明這個算法的復雜度。

首先空間復雜度應該是O(n),因為函數遞歸發生在空間分配之前,因此當函數從下級遞歸中跳出時,最多持有與lists中保存的整數的拷貝(保存在part1和part2中),在combine函數中又會分配一次空間,但也最多只是part1和part2的長度的加總,因此,rec函數同時占據的空間量不會超過2n=O(n)。再提一下,這裏由於沒有要求我們不能使用傳入的鏈表,因此假如我們直接通過操作鏈表進行排序而不分配額外的空間,那麽空間復雜度就可以被優化到O(1)。

時間復雜度我們必須展開來看。一種簡明的方式是將不同的遞歸深度區分開來 ,在同一遞歸深度中的時間復雜度為O(n)。比方說在第一級遞歸,我們對長度為k的lists進行合並,但是實際上發生在這一遞歸深度的只有調用combine的合並操作,其時間復雜度為O(n)。同樣在第二級遞歸,我們分別對0~k/2和k/2~k進行排序,而發生在這一遞歸深度的實際的操作只有combine操作,兩個combine操作正好涉及了lists[0~k]中所有的元素,因此時間復雜度也是O(n)。考慮到遞歸深度最大為log2(k),因此總的時間復雜度為O(n)*log2(k)=O(nlog2(k))。


最後提供一下java代碼,13ms AC:

技術分享
 1 /**
 2  * Definition for singly-linked list.
 3  * public class ListNode {
 4  *     int val;
 5  *     ListNode next;
 6  *     ListNode(int x) { val = x; }
 7  * }
 8  */
 9 public class Solution {
10    
11     public ListNode mergeKLists(ListNode[] lists) {
12         if (lists.length <= 1) {
13             return lists.length == 0 ? null : lists[0];
14         }
15         return mergeKLists(lists, 0, lists.length);
16     }
17 
18     public ListNode mergeKLists(ListNode[] lists, int from, int to) {
19         if (to == from + 1) {
20             return lists[from];
21         }
22         int half = (from + to) / 2;
23         ListNode part1 = mergeKLists(lists, from, half);
24         ListNode part2 = mergeKLists(lists, half, to);
25         if (part1 == null || part2 == null) {
26             return part1 == null ? part2 : part1;
27         }
28         if (part1.val > part2.val) {
29             ListNode tmp = part1;
30             part1 = part2;
31             part2 = tmp;
32         }
33         ListNode traceNode1 = part1;
34         ListNode traceNode2 = part2;
35         ListNode formerNode1 = null;
36         ListNode formerNode2 = null;
37         while (traceNode1 != null && traceNode2 != null) {
38 
39             while (traceNode1 != null && traceNode2 != null && traceNode1.val <= traceNode2.val) {
40                 formerNode1 = traceNode1;
41                 traceNode1 = traceNode1.next;
42             }
43             ListNode start = traceNode2;
44             while (traceNode1 != null && traceNode2 != null && traceNode2.val <= traceNode1.val) {
45                 formerNode2 = traceNode2;
46                 traceNode2 = traceNode2.next;
47             }
48             if (formerNode1 != null) {
49                 formerNode1.next = start;
50                 formerNode1 = formerNode2;
51             }
52             if (formerNode2 != null) {
53                 formerNode2.next = traceNode1;
54                 formerNode2 = null;
55             }
56         }
57         return part1;
58     }
59 }
View Code

Leetcode:Merge k Sorted Lists