【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

公衆號

相關文章
相關標籤/搜索