leetcode 23. 合并K个排序链表(Java版)

43 篇文章 0 订阅
28 篇文章 0 订阅

题目描述(题目难度,困难)

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

示例:

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

示例代码

解法一:

借助优先队列(小根堆),下面解法的时间复杂度为 O ( n log ⁡ ( k ) ) O(n\log(k)) O(nlog(k)),k 为链表个数,n 为总的结点数,空间复杂度为 O ( k ) O(k) O(k),小根堆需要维护一个长度为 k 的数组。
时间复杂度分析:有 k 个结点的完全二叉树,高度为 ⌊ log ⁡ ( k ) ⌋ + 1 \lfloor\log(k)\rfloor+1 log(k)+1,每次弹出堆顶元素和插入元素重新调整堆的时间复杂度都为 O ( log ⁡ ( k ) ) O(\log(k)) O(log(k)),所以总的时间复杂度为 O ( n log ⁡ ( k ) ) O(n\log(k)) O(nlog(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 ( n log ⁡ ( k ) ) O(n\log(k)) O(nlog(k)),k 为链表个数,n 为总的结点数,空间复杂度为 O ( 1 ) O(1) O(1)
时间复杂度分析:两两归并,每个结点会被归并 log ⁡ ( k ) \log(k) log(k) 次,所以总的时间复杂度为 O ( n log ⁡ ( k ) ) O(n\log(k)) O(nlog(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 …;… 直到两归并链表的间距大于等于数组长度为止。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值