【leetcode】合并 k 个有序链表,我给了面试官这 5 种解法

星影.png

开胃菜

在进入本节的正题以前,咱们先来看一道开胃菜。java

题目 21. 合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是经过拼接给定的两个链表的全部节点组成的。 node

示例:git

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

解法 1

思路

直接两个列表合并,排序,而后从新构建一个新的链表。github

  • java 实现
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    List<Integer> numsOne = getIntegerList(l1);
    List<Integer> numsTwo = getIntegerList(l2);
    numsOne.addAll(numsTwo);
    Collections.sort(numsOne);
    // 构建结果
    return buildHead(numsOne);
}

private List<Integer> getIntegerList(ListNode oneNode) {
    // 使用 linkedList,避免扩容
    List<Integer> resultList = new LinkedList<>();
    while (oneNode != null) {
        int value = oneNode.val;
        resultList.add(value);
        oneNode = oneNode.next;
    }
    return resultList;
}
private ListNode buildHead(List<Integer> integers) {
    if(integers.size() == 0) {
        return null;
    }
    ListNode head = new ListNode(integers.get(0));
    ListNode temp = head;
    for(int i = 1; i < integers.size(); i++) {
        temp.next = new ListNode(integers.get(i));
        temp = temp.next;
    }
    return head;
}

效果

Runtime: 4 ms, faster than 22.43% of Java online submissions for Merge Two Sorted Lists.
Memory Usage: 39.6 MB, less than 19.99% of Java online submissions for Merge Two Sorted Lists.

这种思路虽然简单粗暴,可是效果确实不怎么样。算法

那么如何改进呢?数组

主要的问题仍是出在列表原本就是有序的,咱们没有很好的利用这个特性。less

解法 2

思路

直接循环一遍,对比两者的数据大小,充分利用数组有序的特性。ide

实现

public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    if(l1 == null) {
        return l2;
    }
    if(l2 == null) {
        return l1;
    }
    // 临时变量
    ListNode newNode = new ListNode(0);
    // 新增的头指针
    ListNode head = newNode;
    // 循环处理
    while (l1 != null && l2 != null) {
        int valOne = l1.val;
        int valTwo = l2.val;
        // 插入小的元素节点
        if(valOne <= valTwo) {
            newNode.next = l1;
            l1 = l1.next;
        } else {
            newNode.next = l2;
            l2 = l2.next;
        }
        // 变换 newNode
        newNode = newNode.next;
    }
    // 若是长度不同
    if(l1 != null) {
        newNode.next = l1;
    }
    if(l2 != null) {
        newNode.next = l2;
    }
    return head.next;
}

效果

Runtime: 0 ms, faster than 100.00% of Java online submissions for Merge Two Sorted Lists.
Memory Usage: 38.8 MB, less than 88.76% of Java online submissions for Merge Two Sorted Lists.

超过 100% 的提交者,此次还算比较满意。优化

解决了这道开胃菜以后,让咱们一块儿看下后面的正菜。ui

进阶版

  1. 合并K个排序链表

合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。

示例:

输入:
[
  1->4->5,
  1->3->4,
  2->6
]
输出: 1->1->2->3->4->4->5->6

1. BF破万法

思路

咱们按照和 2 个数组相似的策略,所有放在一个列表中,而后排序,最后构建。

  • java 实现

代码很是的简单,以下:

public ListNode mergeKLists(ListNode[] lists) {
    if(null == lists || lists.length == 0) {
        return null;
    }
    // 查找操做比较少
    List<Integer> integerList = new LinkedList<>();
    for(ListNode listNode : lists) {
        integerList.addAll(getIntegerList(listNode));
    }
    // 排序
    Collections.sort(integerList);
    // 构建结果
    return buildHead(integerList);
}

private List<Integer> getIntegerList(ListNode oneNode) {
    // 使用 linkedList,避免扩容
    List<Integer> resultList = new LinkedList<>();
    while (oneNode != null) {
        int value = oneNode.val;
        resultList.add(value);
        oneNode = oneNode.next;
    }
    return resultList;
}

private ListNode buildHead(List<Integer> integers) {
    if(integers.size() == 0) {
        return null;
    }
    ListNode head = new ListNode(integers.get(0));
    ListNode temp = head;
    for(int i = 1; i < integers.size(); i++) {
        temp.next = new ListNode(integers.get(i));
        temp = temp.next;
    }
    return head;
}

效果

Runtime: 103 ms, faster than 15.12% of Java online submissions for Merge k Sorted Lists.
Memory Usage: 40.6 MB, less than 94.79% of Java online submissions for Merge k Sorted Lists.

花了共计 100ms,若是我说咱们的最终版本能够把这个解法提高 100 倍,你信吗?

这个问题和之前同样,那咱们换一种套路。

2. k = (k-1) + 1

思路

实际上 n 个列表合并,咱们能够拆分为两两合并,最后变为一个完整的链表。

实现

public ListNode mergeKLists(ListNode[] lists) {
    if(null == lists || lists.length == 0) {
        return null;
    }
    //
    ListNode result = lists[0];
    // 从第二个开始遍历
    for(int i = 1; i < lists.length; i++) {
        ListNode node = lists[i];
        result = mergeTwoLists(result, node);
    }
    return result;
}

mergeTwoLists 使咱们在两个链表合并中的最佳解法,这里复用了一下。

效果

Runtime: 98 ms, faster than 16.31% of Java online submissions for Merge k Sorted Lists.
Memory Usage: 41.4 MB, less than 47.50% of Java online submissions for Merge k Sorted Lists.

改善效果不是很明显。

那么如何改进呢?

实际上这里有个问题就是咱们是依次遍历,(1,2)合并成一个节点,和(3)继续合并。

下面咱们来看一个比较取巧的解法。

3. 优先级队列-排序我最强

思路

咱们能够借助优先级队列,让咱们的排序从原来的 N 优化为 LogN 。

实现

public ListNode mergeKLists(ListNode[] lists) {
    if(null == lists || lists.length == 0) {
        return null;
    }
    PriorityQueue<ListNode> queue = new PriorityQueue<>(lists.length, new Comparator<ListNode>() {
        @Override
        public int compare(ListNode o1, ListNode o2) {
            return o1.val - o2.val;
        }
    });
    // 循环添加元素
    for(ListNode listNode : lists) {
        if(listNode != null) {
            queue.offer(listNode);
        }
    }
    // 依次弹出
    return buildHead(queue);
}
/**
 * 构建头节点
 * @param queue 列表
 * @return 结果
 * @since v2
 */
private ListNode buildHead(Queue<ListNode> queue) {
    ListNode dummy = new ListNode(0);
    ListNode tail = dummy;
    while (!queue.isEmpty()) {
        tail.next = queue.poll();
        tail = tail.next;
        // 这里相似于将 queue 层层剥开放入 queue 中
        if(tail.next != null) {
            queue.add(tail.next);
        }
    }
    return dummy.next;
}

效果

Runtime: 4 ms, faster than 81.55% of Java online submissions for Merge k Sorted Lists.
Memory Usage: 41.1 MB, less than 74.81% of Java online submissions for Merge k Sorted Lists.

4ms! 此次简直是质的飞跃,从 100ms 提高了 25 倍左右。可喜可贺。

那么,咱们会止步于此吗?

还可以更上一层楼吗?

4. 分治-分而治之,各个击破

  • 思想

这种 k 个有序链表的问题,其实均可以拆分为更小的子问题。

全部相似的问题,基本上均可以使用 DP 或者分治的方式来解决。

本次展现一下分治算法,将合并的链表从中间拆分为二个部分处理。

实现

public ListNode mergeKLists(ListNode[] lists) {
    final int length = lists.length;
    if(lists.length == 0) {
        return null;
    }
    if(lists.length == 1) {
        return lists[0];
    }
    // 递归获取两个节点
    int mid = (length) / 2;
    ListNode one = mergeKLists(subArray(lists, 0, mid));
    ListNode two = mergeKLists(subArray(lists, mid, length));
    // 合并最后2个节点
    return mergeTwoLists(one, two);
}

private ListNode[] subArray(ListNode[] listNodes, int start, int end) {
    int size = end-start;
    ListNode[] result = new ListNode[size];
    int index = 0;
    for(int i = start; i < end; i++) {
        result[index++] = listNodes[i];
    }
    return result;
}

效果

Runtime: 2 ms, faster than 91.66% of Java online submissions for Merge k Sorted Lists.
Memory Usage: 41.5 MB, less than 34.83% of Java online submissions for Merge k Sorted Lists.

2ms! 咱们又把速度提高了一倍,这下你满意了吗?

无论你满不满意,我不满意,由于还没作到最好。

最明显的一个地方就是咱们为了使用分治,对数组进行复制拷贝,这种复制其实是很消耗时间的,那么又没有办法能够解决呢?

5. 优化的尽头

思路

咱们分治是把数组分为左右两个部分,实际上咱们有另外一种办法也能够达到相似的效果。

好比:

[1, 2, 3, 4]

咱们能够首位结合:

[(1,4), 2, 3]
[(1,4,3), 2]
[(1,4,3,2)]

这样能够达到一样的效果,也避免了空间的浪费,和时间的消耗。

实现

public ListNode mergeKLists(ListNode[] lists) {
    if (lists.length == 0) {
        return null;
    }
    int i = 0;
    int j = lists.length - 1;
    while (j > 0) {
        // ?
        i = 0;
        while (i < j) {
            lists[i] = mergeTwoLists(lists[i], lists[j]);
            i++;
            j--;
        }
    }
    return lists[0];
}

效果

Runtime: 1 ms, faster than 100.00% of Java online submissions for Merge k Sorted Lists.
Memory Usage: 41.5 MB, less than 35.98% of Java online submissions for Merge k Sorted Lists.

1ms! 咱们将这个算法从 100ms 优化到 1ms。

可见有时候 cpu 核数翻倍,也没有一个优秀的算法来的效果显著,这也正是算法的威力。

夜已经深了,本次解析先到这里,后续将深刻讲解一下本文提到的优先级队列。

若是你对这个算法不满意,在保住头发的前提下,请继续优化~

拓展阅读

优先级队列 Priority Queue

优先级队列与堆排序

leetcode 源码实现

参考资料

https://leetcode.com/problems/merge-k-sorted-lists/submissions/

https://leetcode-cn.com/problems/merge-two-sorted-lists

公众号

相关文章
相关标签/搜索