时间复杂度的一般计算方式在之前的文章数据结构与算法(一)——时间复杂度讲过。一般算法的时间复杂度都可以通过之前的方法计算,不过还是有一些算法,它的时间复杂度比较难计算。下面就以非常常见的面试题合并K个升序链表 为例:
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
合并两个排序链表
这里先来个简化版,先来看看如何实现两个排序链表合并。如图
这个算法题比较简单,只要遍历链表A、B就行,然后比较就行。需要注意的是需要考虑链表A、B长度不同的情况。代码如下:
private ListNode mergeTwoList(ListNode f,ListNode s){
if(f == null)return s;
if(s == null)return f;
ListNode head = new ListNode(-1);
ListNode p = head;
while(f != null && s!=null){
if(f.val <= s.val){
p.next = f;
f = f.next;
}else{
p.next = s;
s = s.next;
}
p = p.next;
}
if(f != null){
p.next = f;
}
if(s != null){
p.next = s;
}
return head.next;
}
现在我们来看看它的时间复杂度是多少。可以看出每次循环迭代中,链表 A 和 链表 B 只有一个元素会被放进合并链表中, 因此 while 循环的次数不会超过两个链表的长度之和。所有其他操作的时间复杂度都是常数级别的,因此总的时间复杂度为 O(n+m)。是不是很简单,但是当我们合并k个有序的链表时就不会这么简单了。
合并k个有序的链表
当我们合并k个有序链表时,我们应该怎么做呢。最简单的方法就是将两两链表顺序合并,如图:
这种方式非常简单,直接上代码:
public ListNode mergeKLists(ListNode[] lists) {
if(lists == null||lists.length <= 0)return null;
if(lists.length == 1)return lists[0];
ListNode f = lists[0];
for(int i = 1;i < lists.length;i++){
f = mergeTwoList(f,lists[i]);
}
return f;
}
//合并两个排序链表
private ListNode mergeTwoList(ListNode f,ListNode s){
if(f == null)return s;
if(s == null)return f;
ListNode head = new ListNode(-1);
ListNode p = head;
while(f != null && s!=null){
if(f.val <= s.val){
p.next = f;
f = f.next;
}else{
p.next = s;
s = s.next;
}
p = p.next;
}
if(f != null){
p.next = f;
}
if(s != null){
p.next = s;
}
return head.next;
}
那么这种算法的时间复杂度是多少呢,我们来算一算。
假设 k 个链表的长度分别为 n1,n2,n3,...,nk;根据代码可知:
- 第一次执行
mergeTwoList方法的时间为:n1+n2 - 第二次执行
mergeTwoList方法的时间为:n1+n2+n3 - 第三次执行
mergeTwoList方法的时间为:n1+n2+n3+n4 - ...
- 第k-1次执行
mergeTwoList方法的时间为:n1+n2+n3+n4+...+nk
这里我们假设最长的链表长度为 n。则上面的换算为如下表达式:
- 第一次执行
mergeTwoList方法的时间为:n1+n2 <= 2*n - 第二次执行
mergeTwoList方法的时间为:n1+n2+n3 <= 3*n - 第三次执行
mergeTwoList方法的时间为:n1+n2+n3+n4 <= 4*n - ...
- 第k-1次执行
mergeTwoList方法的时间为:n1+n2+n3+n4+...+nk <= k*n
其总时间为 <= ((1+k)*(k-1)/2)*n,即时间复杂度为O(n*k^2)
上面的方法非常简单,但是时间复杂度也非常高。我们可以使用分治法降低复杂度,说起来好像非常高端,但是实际的实现方法非常简单。如图所示,将n个链表以中间为对称,合并链表,如此循环直至合并成一个完整链表。
代码如下:
public ListNode mergeKLists(ListNode[] lists) {
int len = lists.length;
if (len == 0) {
return null;
}
// 将n个链表以中间为对称,合并
while(len>1) {
for (int i=0; i<len/2; i++) {
lists[i] = mergeTwoList(lists[i], lists[len-1-i]);
}
len = (len+1)/2;
}
return lists[0];
}
//合并两个排序链表
private ListNode mergeTwoList(ListNode f,ListNode s){
if(f == null)return s;
if(s == null)return f;
ListNode head = new ListNode(-1);
ListNode p = head;
while(f != null && s!=null){
if(f.val <= s.val){
p.next = f;
f = f.next;
}else{
p.next = s;
s = s.next;
}
p = p.next;
}
if(f != null){
p.next = f;
}
if(s != null){
p.next = s;
}
return head.next;
}
这篇文章主要是讲时间复杂度,实现方式就不介绍了。下面我们来看看它的时间复杂度是多少。
为了方便,假设其最长的链表长度为 n 。先看这段代码:
while(n > 1){
n = n / 2;
}
假设执行g次,则 n / (2^g) = 1,则 g = log n。
由于链表个数为 k ,同理下面这段代码while部分要执行 logk次
while(len>1) {
for (int i=0; i<len/2; i++) {
lists[i] = mergeTwoList(lists[i], lists[len-1-i]);
}
len = (len+1)/2;
}
- 第一次执行:
(2*n)*(k/2)= k*n - 第二次执行:
(4*n)*(k/4)= k*n - ...
- 第logk次执行:
(k*n)*1= k*n
其总时间 <=k*n*logk,即时间复杂度为O(k*n*logk)