算法分析与设计,第7周博客
23. Merge k Sorted Lists
Merge k sorted linked lists and return it as one sorted list. Analyze and describe its complexity.
题意是,给出k个已排好序的链表,把它们合并成一个链表。看完题目以后,可能不是很清楚,但是看到给出的函数接口会更加的明白。所给出的代码如下:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
}
}
给出了k个链表的头节点组成的数组lists,其中的每个元素是每个链表的head节点,这样一来,题目的意思就很清楚了。
首先,考虑最简单的办法,每次取数组里面最小的那个,放在已排序的链表的尾部,然后再把这个节点改成它的下一个节点放在数组里,如此反复直到每个链表都到了尾部。这个方法的代码如下:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
ListNode head(ListNode[] cur) {
int k = cur.length;
ListNode res = new ListNode(Integer.MAX_VALUE);
int min = 0;
for (int i = 0; i < k; ++i) {
if (cur[i] != null && (cur[min] == null || cur[i].val < cur[min].val)) {
min = i;
}
}
res = cur[min];
cur[min] = cur[min].next;
return res;
}
boolean over(ListNode[] cur) {
for (int i = 0; i < cur.length; ++i) {
if (cur[i] != null)
return false;
}
return true;
}
public ListNode mergeKLists(ListNode[] lists) {
int k = lists.length;
ListNode head = null;
ListNode cur = null;
while (!over(lists)) {
if (head == null) {
cur = head = head(lists);
} else {
cur.next = head(lists);
cur = cur.next;
}
}
return head;
}
}
来看下这个算法的时间复杂度,设数组的大小为k,总的链表的长度为n,每选出一个节点,都要调用一次head函数,head函数的时间复杂度是O(k),一共有n个节点,所以总的时间复杂度是O(n*k)。这个算法并不是一个非常有效率的,在测试的时候也确实超时了。
我们的思路是没有问题的,那么,怎么能够降低这个算法的时间复杂度呢。如果降低head函数的时间复杂度呢?在之前的代码中,head函数是通过遍历的方式来得到最小的节点,所以时间复杂度是O(k)。因为我们每次都只需要最小的节点,而不需要保持整个数组的有序性,所以很容易就可以联想到最小堆。最小堆是这样的一棵二叉树,每个父节点都比左节点和右节点要小。最重要的是当我们取出整个最小堆的根节点,并插入一个新的节点时,所需要的时间复杂度是O(long(k))。这样,我们就把head函数的时间复杂度从O(k)缩小到了O(log(k))。所需要的就是维护一个最小堆。维护最小堆的代码如下:
void rebulidHeap(ListNode[] list, int s) {
int left = 2*s+1;
int right = 2*s+2;
if (left >= list.length)
return;
int small = left;
if (right < list.length && list[right] != null && (list[small] == null || list[right].val < list[small].val))
small = right;
if (list[small] != null && (list[s] == null || list[small].val < list[s].val)) {
swap(list, small, s);
rebulidHeap(list, small);
}
}
在整个程序的开始之初,还需要建堆,把这个数组变成一个最小堆。代码如下:
void bulidHeap(ListNode[] list) {
for (int i = list.length-1; i > 0; --i) {
if (list[i] == null)
continue;
int parent = (i-1)/2;
if (list[parent] == null || list[i].val < list[parent].val) {
swap(list, i, parent);
rebulidHeap(list, i);
}
}
}
而建堆的时间复杂度是O(k*log(k))。
总体的代码如下:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
void rebulidHeap(ListNode[] list, int s) {
int left = 2*s+1;
int right = 2*s+2;
if (left >= list.length)
return;
int small = left;
if (right < list.length && list[right] != null && (list[small] == null || list[right].val < list[small].val))
small = right;
if (list[small] != null && (list[s] == null || list[small].val < list[s].val)) {
swap(list, small, s);
rebulidHeap(list, small);
}
}
void bulidHeap(ListNode[] list) {
for (int i = list.length-1; i > 0; --i) {
if (list[i] == null)
continue;
int parent = (i-1)/2;
if (list[parent] == null || list[i].val < list[parent].val) {
swap(list, i, parent);
rebulidHeap(list, i);
}
}
}
void swap(ListNode[] list, int i, int j) {
ListNode tmp = list[i];
list[i] = list[j];
list[j] = tmp;
}
public ListNode mergeKLists(ListNode[] lists) {
int k = lists.length;
ListNode head = null;
ListNode cur = null;
bulidHeap(lists);
while (lists.length > 0 && lists[0] != null) {
if (head == null) {
cur = head = lists[0];
} else {
cur.next = lists[0];
cur = cur.next;
}
lists[0] = lists[0].next;
rebulidHeap(lists, 0);
}
return head;
}
}
再来看下这个算法的时间复杂度,建堆的时间复杂度是O(k*log(k)),合并链表的时间复杂度是O(n*log(k)),所以总的时间复杂度是O((k+n)*log(k))。