leetcode 23. 合併K個排序鏈表(Java版)

題目描述(題目難度,困難)

合併 k 個排序鏈表,返回合併後的排序鏈表。請分析和描述算法的複雜度。

示例:

輸入:
[
1->4->5,
1->3->4,
2->6
]
輸出: 1->1->2->3->4->4->5->6

示例代碼

解法一:

藉助優先隊列(小根堆),下面解法的時間複雜度爲 O(nlog(k))O(n\log(k)),k 爲鏈表個數,n 爲總的結點數,空間複雜度爲 O(k)O(k),小根堆需要維護一個長度爲 k 的數組。
時間複雜度分析:有 k 個結點的完全二叉樹,高度爲 log(k)+1\lfloor\log(k)\rfloor+1,每次彈出堆頂元素和插入元素重新調整堆的時間複雜度都爲 O(log(k))O(\log(k)),所以總的時間複雜度爲 O(nlog(k))O(n\log(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;
	}
}

解法二:

類似歸併排序的回溯過程,兩兩合併。下面解法的時間複雜度也爲 O(nlog(k))O(n\log(k)),k 爲鏈表個數,n 爲總的結點數,空間複雜度爲 O(1)O(1)
時間複雜度分析:兩兩歸併,每個結點會被歸併 log(k)\log(k) 次,所以總的時間複雜度爲 O(nlog(k))O(n\log(k))

/**
 * 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 …;… 直到兩歸併鏈表的間距大於等於數組長度爲止。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章