題目描述(題目難度,困難)
合併 k 個排序鏈表,返回合併後的排序鏈表。請分析和描述算法的複雜度。
示例:
輸入:
[
1->4->5,
1->3->4,
2->6
]
輸出: 1->1->2->3->4->4->5->6
示例代碼
解法一:
藉助優先隊列(小根堆),下面解法的時間複雜度爲 ,k 爲鏈表個數,n 爲總的結點數,空間複雜度爲 ,小根堆需要維護一個長度爲 k 的數組。
時間複雜度分析:有 k 個結點的完全二叉樹,高度爲 ,每次彈出堆頂元素和插入元素重新調整堆的時間複雜度都爲 ,所以總的時間複雜度爲 。分析的比較粗略,不是精確的時間複雜度,不過大 O 表示法本身就是一個粗略的量級的時間複雜度表示,這樣就足夠了。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> pq = new PriorityQueue<>(new Comparator<ListNode>() {
@Override
public int compare(ListNode o1, ListNode o2) {
return o1.val-o2.val;
}
});
for(ListNode e : lists){
if(e != null){
pq.add(e);
}
}
ListNode head = new ListNode(0);
head.next = null;
ListNode tail = head;
while(!pq.isEmpty()){
tail.next = pq.poll();
tail = tail.next;
if(tail.next != null){
pq.add(tail.next);
}
tail.next = null;
}
return head.next;
}
}
解法二:
類似歸併排序的回溯過程,兩兩合併。下面解法的時間複雜度也爲 ,k 爲鏈表個數,n 爲總的結點數,空間複雜度爲 。
時間複雜度分析:兩兩歸併,每個結點會被歸併 次,所以總的時間複雜度爲 。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
// 藉助歸併排序的思想,只有治沒有分
public ListNode merge(ListNode l1, ListNode l2){
ListNode res = new ListNode(0);
ListNode tail = res;
while(l1 != null && l2 != null){
if(l1.val < l2.val){
tail.next = l1;
l1 = l1.next;
}else {
tail.next = l2;
l2 = l2.next;
}
tail = tail.next;
}
if(l1 != null){
tail.next = l1;
}else{
tail.next = l2;
}
return res.next;
}
// 原地歸併,並不申請新的數組空間,算法實現上,其實是找規律。
public ListNode mergeKLists(ListNode[] lists) {
// 步長爲 2 時,和後面的第 1 個合併
// 步長爲 4 時,和後面的第 2 個合併
// ...
if(lists == null){
return null;
}
int len = lists.length;
int interval = 1;
while(interval < len){
for(int i = 0; i+interval < len; i += 2*interval){
lists[i] = merge(lists[i], lists[i + interval]);
}
interval *= 2;
}
return len != 0 ? lists[0] : null;
}
}
思路解析
對於解法一:
由於我們很熟悉兩個有序鏈表的合併,每次兩兩比較,將較小值合併進最終的有序鏈表。問題推廣到 k 個有序鏈表的合併,我們自然而然的可以想到,每次比較 k 個值,將其中最小的合併進最終的有序鏈表。所以我們可以使用一個優先隊列(小根堆)來維護 k 個結點的大小信息,而不需要每次從頭開始比較 k 個結點的大小。
對於解法二:
k 個有序鏈表合併這個問題,可以看作是歸併排序回溯過程中的一個狀態,使用分治的思想求解,不過和歸併排序不同的是,這裏只有治而沒有分。
下面這個圖詳細體現了算法過程,並且我們可以原地歸併,不需要申請新的數組空間:
原地歸併的算法實現,其實就是一個找規律問題。第一輪的歸併是,0 和 1,2 和 3,4 和 5 …;第二輪的歸併是,0 和 2,4 和 6 …;第三輪的歸併是,0 和 4,4 和 8 …;… 直到兩歸併鏈表的間距大於等於數組長度爲止。