数据结构和算法(九)时间复杂度实例分析

400 阅读1分钟

时间复杂度的一般计算方式在之前的文章数据结构与算法(一)——时间复杂度讲过。一般算法的时间复杂度都可以通过之前的方法计算,不过还是有一些算法,它的时间复杂度比较难计算。下面就以非常常见的面试题合并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)