23. 合并K个排序链表---最小堆,归并分治

合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。

示例:

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

 

方法一:贪心算法、优先队列

思路分析:

1、由于是 k个排序链表,那么这 k 个排序的链表头结点中 val 最小的结点就是合并以后的链表中最小的结点;

2、最小结点所在的链表的头结点就要更新了,更新成最小结点的下一个结点(如果有的话),此时还是这 k个链表,这 k个排序的链表头结点中 val 最小的结点就是合并以后的链表中第 2 小的结点。

写到这里,我想你应该差不多明白了,我们每一次都从这 kkk 个排序的链表头结点中拿出 val 最小的结点“穿针引线”成新的链表,这个链表就是题目要求的“合并后的排序链表”。“局部最优,全局就最优”,这不就是贪心算法的思想吗。

下面的图解释了上面的思路。

23. 合并K个排序链表---最小堆,归并分治_第1张图片

 

23. 合并K个排序链表---最小堆,归并分治_第2张图片

23. 合并K个排序链表---最小堆,归并分治_第3张图片

public class ABCTest {
	public ListNode mergeKLists(ListNode[] lists) {
		PriorityQueue pq = new PriorityQueue<>(new Comparator() {
			@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 为总的结点数,空间复杂度为 ,小根堆需要维护一个长度为 k 的数组。
时间复杂度分析:有 k 个结点的完全二叉树,高度为 ,每次弹出堆顶元素和插入元素重新调整堆的时间复杂度都为 ,所以总的时间复杂度为 。分析的比较粗略,不是精确的时间复杂度,不过大 O 表示法本身就是一个粗略的量级的时间复杂度表示,这样就足够了。

 

 

 

 

方法 2:分治

k 个有序链表合并这个问题,可以看作是归并排序回溯过程中的一个状态,使用分治的思想求解,不过和归并排序不同的是,这里只有治而没有分。
下面这个图详细体现了算法过程,并且我们可以原地归并,不需要申请新的数组空间:

23. 合并K个排序链表---最小堆,归并分治_第4张图片

原地归并的算法实现,其实就是一个找规律问题。第一轮的归并是,0 和 1,2 和 3,4 和 5 ...;第二轮的归并是,0 和 2,4 和 6 ...;第三轮的归并是,0 和 4,4 和 8 ...;... 直到两归并链表的间距大于等于数组长度为止。

 

public class ABCTest {
	//借助归并排序的思想,只有治没有分
	public ListNode mergeKLists(ListNode[] lists) {
		if(lists==null)
			return null;
		int len = lists.length; //lists数组中用“,”隔开的list个数,本题举例中为3
		int interval = 1;
		while(interval

类似归并排序的回溯过程,两两合并。下面解法的时间复杂度也为 ,k 为链表个数,n 为总的结点数,空间复杂度为 。
时间复杂度分析:两两归并,每个结点会被归并 次,所以总的时间复杂度为 。 

 

你可能感兴趣的:(23. 合并K个排序链表---最小堆,归并分治)