05 August 2008
Merge k sorted linked lists and return it as one sorted list. Analyze and describe its complexity.
Example:
1
2
3
4
5
6
7
Input:
[
1->4->5,
1->3->4,
2->6
]
Output: 1->1->2->3->4->4->5->6
这道题是多个有序链表合并成一个,在分布式系统中比较常见,比如来自不同客户端的sorted lists数据要在central server上面merge起来。
这个题目一般有两种做法:
1)第一种做法比较容易想到,就是有点类似于Merge Sort的思路,就是分治法 - 很重要的经典的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 + n)。
这里来复习一下Merge Sort(对于数组操作),归并操作(merge),也叫归并算法,指的是将两个已经排序的序列合并成一个序列的操作。归并排序算法依赖归并操作。
归并操作的过程如下:
最差時間复杂度 | |
---|---|
最优時間复杂度 | |
平均时间复杂度 | |
最差空间复杂度 |
2)第二种方法。这种方法用到了堆的数据结构,但是其实原理比较简单。维护一个大小为k的堆,每次取堆顶的最小元素放到最后需要返回的链表中,然后读取该元素的下一个元素放入堆中,重新维护好。因为每个链表是有序的,每次又是取当前k个元素中最小的(k个list),所以当所有链表都读完时就结束了。这个时候所有元素就是按从小到大放入到结果链表中。这个算法每个元素要读取一次,即是_k_n次,然后每次读取元素要把新元素插入堆中要logk的复杂度,所以总时间复杂度是O(nklogk)。空间复杂度是堆的大小,即为O(k)。
两种方法有着同样的时间复杂度,都是可以接受的解法,但是却代表了两种不同的思路,数据结构也不同。两种方法都应该掌握,因为在实际中比较有应用,所以也是比较常考的题目。
堆 Heap
堆实质上是利用完全二叉树(只有最下面的两层结点度能够小于2,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树,所有操作均为对数log级别)来维护一组数据的结果,这个完全二叉树满足这些条件:树中任一非叶结点的关键字均不大于(或不小于)其左右孩子(若存在)结点的关键字。所以堆是完全二叉树,分为小根堆和大根堆,如果任意节点的左右孩子(若有)值都不小于其父亲,这是最小堆(min heap,也叫小根堆),即最小的永远在上面;相反的是最大堆(max heap,也叫大根堆),即父节点大于两个子节点。
因为完全二叉树有很好的规律,因此可以只用数据来存储数据而不需要链表。
优先队列 Priority Queue
队列Queue是一个操作受限的线性表(Java中default的优先队列由min heap来实现),数据只能在一端进入,另一端出来,具有先进先出FIFO的性质。有时在队列中需要处理优先级的情况,即后面进入的数据需要提前出来,这时候就需要优先队列。优先队列是至少能够提供插入和获取最小值(获取数值同时删除)这两种操作的数据结构。对应于队列的操作,插入相当于入队,删除最小值相当于出队。
链表Linked List,二分查找树BST,都可以提供插入和取得最小值这两种操作。对于链表的实现,插入只需要O(1),删除最小值需要遍历链表(无论单向还是双向链表都一样,遍历两次),故需要O(N)。对于二叉查找树,这两种操作都需要O(logN);而且随着不停的删除最小的操作,二叉查找树会变得非常不平衡;同时使用二叉查找树有些浪费,因此很多操作根本不需要。
分治法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) {
return null;
}
return divideAndConquer(lists, 0, lists.length - 1);
}
// 分治
private ListNode divideAndConquer(ListNode[] lists, int left, int right) {
if (left >= right) { // 需要包含等于(大于条件不会达到),表示自己和自己不用merge,直接返回当前链表
return lists[left];
}
int mid = left + (right - left) / 2;
ListNode leftList = divideAndConquer(lists, left, mid);
ListNode rightList = divideAndConquer(lists, mid + 1, right);
return mergeTwoSortedLists(leftList, rightList); //在递归栈中从底到上lists进行两两合并
}
// "治",合并两个有序链表
private ListNode mergeTwoSortedLists(ListNode list0, ListNode list1) {
if (list0 == null) {
return list1;
}
if (list1 == null) {
return list0;
}
ListNode dummy = new ListNode(-1);
ListNode current = dummy;
while (list0 != null && list1 != null) {
if (list0.val <= list1.val) {
current.next = list0;
list0 = list0.next;
} else {
current.next = list1;
list1 = list1.next;
}
current = current.next;
}
current.next = (list0 == null) ? list1 : list0;
return dummy.next;
}
}
堆的办法,用优先队列实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) {
return null;
}
// default升序,size为lists的大小
PriorityQueue<ListNode> minHeap = new PriorityQueue<>(lists.length, Comparator.comparingInt(list -> list.val));
// 类似合并两个有序链表的非递归办法,需要创建dummy(递归解法不用创建)
ListNode dummy = new ListNode(-1);
ListNode current = dummy;
// 首先遍历一次所有list的头节点,加入到最小堆中
for (ListNode node : lists) {
if (node != null) { // 这个条件很重要,排除空链表
minHeap.offer(node);
}
}
while (!minHeap.isEmpty()) {
current.next = minHeap.poll();
current = current.next; // 赶紧顺杆往上爬,在有当前最小值的链表上往后检查一步
if (current.next != null) { // 非空才加入进行自平衡
minHeap.offer(current.next);
}
}
return dummy.next;
}
}