Merge k sorted linked lists and return it as one sorted list. Analyze and describe its complexity.
Hide Tags
Divide and Conquer Linked List Heap
首先容易想到的就是首先容易想到的就是讲链表里的所有数都遍历一遍存到一个list中,再对list进行排序,然后新建一个链表,将list中的数值不断的赋给ListNode的val即可。
但是这种的方法的时间复杂度主要有对list排序这一步决定。k个链表,每个链表最多n个元素,那么排序的复杂度就是O(nklog(nk)),未经优化,比网上的O(nklogk)要高,
网上用的最多的就是利用分治的思想进行归并,或者利用最小堆的思想,每次得到所有链表中的最小值。
下面用基于最小堆的优先队列(每次出队的是优先级最高的,这里对应最小的ListNode)来解决,但是运行时间(428ms)貌似比上面(392ms)还多。
这道题目在分布式系统中非常常见,来自不同client的sorted list要在central server上面merge起来。这个题目一般有两种做法,下面一一介绍并且分析复杂度。 第一种做法比较容易想到,就是有点类似于MergeSort的思路,就是分治法,不了解MergeSort的朋友,请参见 归并排序-维基百科 ,是一个比较经典的O(nlogn)的排序算法,还是比较重要的。思路是先分成两个子任务,然后递归求子任务,最后回溯回来。这个题目也是这样,先把k个list分成两半,然后继续划分,知道剩下两个list就合并起来,合并时会用到 Merge Two Sorted Lists 这道题,不熟悉的朋友可以复习一下。
Hide Tags
Divide and Conquer Linked List Heap
首先容易想到的就是首先容易想到的就是讲链表里的所有数都遍历一遍存到一个list中,再对list进行排序,然后新建一个链表,将list中的数值不断的赋给ListNode的val即可。
但是这种的方法的时间复杂度主要有对list排序这一步决定。k个链表,每个链表最多n个元素,那么排序的复杂度就是O(nklog(nk)),未经优化,比网上的O(nklogk)要高,
网上用的最多的就是利用分治的思想进行归并,或者利用最小堆的思想,每次得到所有链表中的最小值。
未经优化的代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
public class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists.length==0) return null;
ArrayList<Integer> arrayList = new ArrayList<Integer>();
ListNode p = null;
for(int i=0;i<lists.length;i++) {
p = lists[i];
for(int j =0;p!=null; p = p.next) {
arrayList.add(p.val);
}
}
Collections.sort(arrayList);
ListNode head = new ListNode(0);
ListNode q = head;
for(int i=0;i<arrayList.size();i++) {
q.next = new ListNode(arrayList.get(i));
q = q.next;
}
return head.next;
}
}
下面用基于最小堆的优先队列(每次出队的是优先级最高的,这里对应最小的ListNode)来解决,但是运行时间(428ms)貌似比上面(392ms)还多。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
public class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists==null || lists.length==0) return null;
PriorityQueue<ListNode> q = new PriorityQueue<ListNode>(lists.length,new Comparator<ListNode>(){
public int compare(ListNode a,ListNode b) { //需要传入一个比较器才能对ListNode对象进行比较
return a.val-b.val;
}
});
for(int i=0;i<lists.length;i++) {
if(lists[i]!=null) //需要判断一下,才能添加,否则报空指针异常
{q.add(lists[i]);}
}
ListNode head = new ListNode(0);
ListNode p = head;
while(q.size()>0) {
ListNode temp = q.poll(); //出队的操作
p.next = temp;
if(temp.next!=null) {
q.add(temp.next);
}
p = p.next;
}
return head.next;
}
}
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
public class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists==null || lists.length==0) return null;
int length = lists.length;
return helper(lists,0,length-1);
}
public ListNode helper(ListNode[] lists,int left,int right) {
if(left<right) {
int mid = (left+right)/2;
return merge(helper(lists,left,mid),helper(lists,mid+1,right)); //二分递归的过程
}
return lists[left]; //处理左右两端相同的情况即left==right
}
public ListNode merge(ListNode a,ListNode b) {
ListNode p = a;
ListNode q = b;
ListNode head = new ListNode(0);
ListNode h = head;
while(p!=null && q!=null) { //合并2个list
if(p.val<q.val) {
h.next = p;
p =p.next;
}else{
h.next = q;
q = q.next;
}
h = h.next;
}
if(p==null) {
h.next = q;
} else{
h.next = p;
}
return head.next;
}
}
这道题目在分布式系统中非常常见,来自不同client的sorted list要在central server上面merge起来。这个题目一般有两种做法,下面一一介绍并且分析复杂度。 第一种做法比较容易想到,就是有点类似于MergeSort的思路,就是分治法,不了解MergeSort的朋友,请参见 归并排序-维基百科 ,是一个比较经典的O(nlogn)的排序算法,还是比较重要的。思路是先分成两个子任务,然后递归求子任务,最后回溯回来。这个题目也是这样,先把k个list分成两半,然后继续划分,知道剩下两个list就合并起来,合并时会用到 Merge Two Sorted Lists 这道题,不熟悉的朋友可以复习一下。
我们来分析一下上述算法的时间复杂度。假设总共有k个list,每个list的最大长度是n,那么运行时间满足递推式T(k) = 2T(k/2)+O(n*k)。根据主定理,可以算出算法的总复杂度是O(nklogk)。如果不了解主定理的朋友,可以参见 主定理-维基百科 。空间复杂度的话是递归栈的大小O(logk)。
接下来我们来看第二种方法。这种方法用到了堆的数据结构,思路比较难想到,但是其实原理比较简单。维护一个大小为k的堆,每次去堆顶的最小元素放到结果中,然后读取该元素的下一个元素放入堆中,重新维护好。因为每个链表是有序的,每次又是去当前k个元素中最小的,所以当所有链表都读完时结束,这个时候所有元素按从小到大放在结果链表中。这个算法每个元素要读取一次,即是k*n次,然后每次读取元素要把新元素插入堆中要logk的复杂度,所以总时间复杂度是O(nklogk)。空间复杂度是堆的大小,即为O(k)。