【数据结构】链表04:LeetCode 21. 合并两个有序链表、LeetCode 23. 合并K个升序链表

一、LeetCode 21. 合并两个有序链表

方法一:迭代

代码与性能

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(-1);
        ListNode p = dummy;
        ListNode p1 = l1;
        ListNode p2 = l2;
        while(p1 != null && p2 != null){
            if(p1.val < p2.val){
                p.next = p1;
                p1 = p1.next;
            }else{
                p.next = p2;
                p2 = p2.next;
            }
            p = p.next;
        }
        p.next = p1 == null ? p2 : p1;
        return dummy.next;
    }
}

时间复杂度: O(m+n)
空间复杂度: O(1)

拓展:如果要求去重

l1.val == l2.val的情况下,只输出一个,然后两个链表同时往下走就好了。

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,2,3,4]

代码:

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(-1);
        ListNode p = dummy;
        ListNode p1 = l1;
        ListNode p2 = l2;
        while(p1 != null && p2 != null){
            if(p1.val < p2.val){
                p.next = p1;
                p1 = p1.next;
            }else if(p1.val == p2.val){
                p.next = p1;
                p1 = p1.next;
                p2 = p2.next;
            }else{
                p.next = p2;
                p2 = p2.next;
            }
            p = p.next;
        }
        p.next = p1 == null ? p2 : p1;
        return dummy.next;
    }
}

方法二:递归

代码与性能

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if(list1 == null) return list2;
        if(list2 == null) return list1;
        if(list1.val < list2.val){
            list1.next = mergeTwoLists(list1.next, list2);
            return list1;
        }else{
            list2.next = mergeTwoLists(list1, list2.next);
            return list2;
        }
    }
}

**时间复杂度:**每次递归的操作:去掉 l1 或者 l2 的头节点(直到至少有一个链表为空),函数 mergeTwoList 至多只会递归调用每个节点一次。因此,时间复杂度取决于合并后的链表长度,即 O(n+m)。
**空间复杂度:**结束递归调用时mergeTwoLists函数最多调用 n+m次,递归需要消耗栈空间,因此空间复杂度为 O(n+m)。

去重版代码

还是同样的思路,改变l1.val == l2.val时的操作。

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if(list1 == null) return list2;
        if(list2 == null) return list1;
        if(list1.val < list2.val){
            list1.next = mergeTwoLists(list1.next, list2);
            return list1;
        }else if(list1.val == list2.val){
            list1.next = mergeTwoLists(list1.next, list2.next);
            return list1;
        }else{
            list2.next = mergeTwoLists(list1, list2.next);
            return list2;
        }
    }
}

二、LeetCode 23. 合并K个升序链表

方法一:归并

1.递归版

以“合并两个有序链表”为原子操作,和归并排序一样。
注意归并的base case:if(left == right) return lists[left];,可以不写成left<=right
因为和二分不同,归并的分区间操作是left = mid + 1right = mid,没有right = mid - 1;又因为mid = left + (right - left) / 2,是向下取整的,所以不会出现left>right的情况。

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        int n = lists.length;
        if(n == 0) return null;
        if(n == 1) return lists[0];
        return merge(lists, 0, n-1);
    }
    ListNode merge(ListNode[] lists, int left, int right){
        if(left == right) return lists[left];
        int mid = left + (right - left) / 2;
        ListNode l1 = merge(lists, left, mid);
        ListNode l2 = merge(lists, mid+1, right);
        return mergeTwoLists(l1, l2);
    }
    //mergeTwoLists方法代码直接复制上一题
    ListNode mergeTwoLists(ListNode l1, ListNode l2);
}

时间复杂度:
和归并排序一个分析思路。设链表的最大长度为n,第一次合并K/2组,每组耗时2n,第二次合并K/4组,每组耗时4n……总共有logK次,每次耗时都是Kn,所以时间复杂度就是O(nK*logK)。其中n为链表最大长度,K为链表条数。
空间复杂度: 主要是递归的栈空间,O(logK)。(注:最好用迭代的mergeTwoLists方法,不会额外占据栈空间)

如果要求去重,只需要把mergeTwoLists方法改成去重版就行了。

2.迭代版

迭代的实现思想和归并排序类似,但是具体实现很不一样。
第一次遍历结束后,在链表数组的前半段,按顺序存储了K/2组链表合并后的结果。
之后每次循环都是这样,实现详见下面代码:

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        int k = lists.length;
        if(k == 0) return null;
        if(k == 1) return lists[0];
        while(k > 1){
            //跟在i后面的指针,i走两下newRight走一下
            //为什么叫这个名,因为它在for循环结束后会成为新的右边界(是开的,不是闭的)
            int newRight = 0;
            for(int i=0; i<k; i+=2){
                //如果k是奇数,则最后会单独留下lists[k-1],不合并直接存下来
                if(i == k-1){
                    lists[newRight++] = lists[i];
                }else{
                    lists[newRight++] = mergeTwoLists(lists[i],lists[i+1]);
                }
            }
            //把newRight赋给k,使k成为新的右边界(开)
            k = newRight;
        }
        //最后链表数组lists的第一个元素就是合并完的数组
        return lists[0];
    }
    //mergeTwoLists方法代码直接复制上一题
    ListNode mergeTwoLists(ListNode l1, ListNode l2);
}

时间复杂度:
和递归版完全一样,O(nKlogK)。
空间复杂度: 主要是递归的栈空间,O(1)。(注:最好用迭代的mergeTwoLists方法,不会额外占据栈空间)

如果要求去重,只需要把mergeTwoLists方法改成去重版就行了。

方法二:优先级队列

1.自己实现优先级队列

合并操作与上一题较为类似。
题解中的MinPQ类实现了一个小顶堆。
如果只是用在本题,那么MinPQ类可以没有Comparator<K>属性。直接通过比较ListNode的val属性来实现more()方法就行。但是为了更加让MinPQ更加通用,我还是选择了模仿JDK的PriorityQueue。MinPQ的通用性就体现在,它的构造器参数中有一个Comparator<K> comparator,而Comparator<T>是一个典型的函数式接口(见这篇文章)。所以,在构造一个MinPQ时,你可以传入一个Lambda表达式来代替参数列表中的Comparator<K> comparator。你可以随意定制Lambda表达式,来改变more()函数的比较机制。比如,在本题的代码中,Lambda表达式是这样的:(node1, node2)->(node1.val - node2.val)
MinPQ没有写grow()方法,不能扩容,然而在本题中没有这个必要。
swim、sink、insert、delMin这四个方法是核心,代码实现来自公众号labuladong的文章《图文详解二叉堆,实现优先级队列》。
另外,如果堆从queue[0]就开始存储,那么left、right、parent方法需要改成:

private int parent(int k){
    return (k-1) / 2;
}
private int left(int k){
    return 2*k + 1;
}
private int right(int k){
    return 2*k + 2;
}

以下为完整题解:

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if(lists.length == 0) return null;
        ListNode dummy = new ListNode(-1);
        ListNode p = dummy;
        MinPQ<ListNode> mp = new MinPQ<>(
                lists.length, (node1, node2)->(node1.val - node2.val));
        //将k个链表的头结点放入最小堆
        for(ListNode head : lists){
            if(head != null) mp.insert(head);
        }
        while(!mp.isEmpty()){
            //找到并连接下一个节点
            ListNode node = mp.delMin();
            p.next = node;
            //移动指针
            if(node.next != null) mp.insert(node.next);
            //p指针不断前进
            p = p.next;
        }
        return dummy.next;
    }
}
//优先级队列,插入或删除元素时,自动排序,队首变成最大的元素
//底层为小顶堆
//小顶堆:完全二叉树,且每个节点的值都小于等于它左右子节点的值
class MinPQ<K> {
    //存储元素的数组
    private K[] queue;
    //当前堆中的元素个数
    private int n;
    //堆的最大容量
    private int capacity;
    //Comparator:典型函数式接口
    private Comparator<K> comparator;
    //构造器
    public MinPQ(int cap, Comparator<K> comparator){
        //索引0不用,所以是cap+1
        this.queue = (K[]) new Object[cap + 1];
        this.n = 0;
        this.capacity = cap;
        this.comparator = comparator;
    }
    //返回堆中最小的元素
    public K min(){
        return queue[1];
    }
    public boolean isEmpty(){
        return n == 0;
    }
    public void insert(K e){
        //如果堆满了,则添加不进去
        if(n >= capacity) return;
        n++;
        queue[n] = e;
        swim(n);
    }
    public K delMin(){
        //如果堆空了,则删除不了,返回null
        if(n == 0) return null;
        //把最小元素换到最后
        exch(1,n);
        //存下原最小元素的值,然后删除
        K min = queue[n];
        queue[n] = null;
        n--;
        sink(1);
        return min;
    }
    private void swim(int k){
        while(k > 1 && more(parent(k),k)){
            exch(k, parent(k));
            k = parent(k);
        }
    }
    private void sink(int k){
        while(left(k) <= n){
            int smaller = left(k);
            if(right(k) <= n && more(left(k),right(k)))
                smaller = right(k);
            if(more(smaller,k))
                break;
            exch(k, smaller);
            k = smaller;
        }
    }
    //交换pq数组中的两个元素
    private void exch(int i, int j){
        K temp = queue[i];
        queue[i] = queue[j];
        queue[j] = temp;
    }
    //判断pq[i]是否大于pq[j]
    private boolean more(int i, int j){
        //不能直接用">"
        return comparator.compare(queue[i],queue[j]) > 0;
    }
    //计算父节点和左右子节点的索引值
    private int parent(int k){
        return k / 2;
    }
    private int left(int k){
        return 2*k;
    }
    private int right(int k){
        return 2*k + 1;
    }
}

2.用JDK的PriorityQueue

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if(lists.length == 0) return null;
        ListNode dummy = new ListNode(-1);
        ListNode p = dummy;
        PriorityQueue<ListNode> pq = new PriorityQueue<>(
                lists.length, (node1, node2)->(node1.val - node2.val));
        //将k个链表的头结点放入最小堆
        for(ListNode head : lists){
            if(head != null) pq.add(head);
        }
        while(!pq.isEmpty()){
            //找到并连接下一个节点
            ListNode node = pq.poll();
            p.next = node;
            //移动指针
            if(node.next != null) pq.add(node.next);
            //p指针不断前进
            p = p.next;
        }
        return dummy.next;
    }
}

3.性能

时间复杂度:优先级队列中元素最多为K个(链表条数),所以一次delMin或insert的时间复杂度为O(logK)。所有链表节点都会被加入和弹出优先级队列,这部分总共耗时O(NlogK),K为链表条数,N为链表节点总数。其他操作如指针的移动,耗时的数量级都不如加入和弹出操作,所以整体时间复杂度就是O(NlogK)。
空间:多存储一个优先级队列,O(K)。
当然这个不太方便去重,去重的话需要改变堆的机制。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值