023 Merge k Sorted Lists[H]

1 題目描述

Merge k sorted linked lists and return it as one sorted list. Analyze and describe its complexity.

難度:Hard

2 題目樣例

無。

3 題意分析

排序K個有序的鏈表。

總體上說來,和之前的Merge Two Sorted Lists類似。

但是鏈表的數量由2攀升至K,難度確實是增加了許多。(直觀感受)

在《演算法導論》一書堆排序的課後題目中,有相同的題目。

注意這裡邊的n是鏈表節點的總數,而本文中,n表示的是每個鏈表平均的節點數

4 思路分析

首先要明確這樣一點......由於此題和之前的Merge Two Sorted Lists的相似性,完全可以把之前的Merge Two Sorted Lists中的操作寫成函數,進行再利用。

(我之前沒有寫成函數形式,感覺血虧。)

1.暴力法:

這是毫無思維難度的一種方法......我在寫代碼之前甚至不能確定這種方法能不能通過。

無腦的把1,2合併,然後把1,2合併的結果再和3合併,1,2,3合併的結果再和4合併......以此類推。

我們可以計算一下這個過程的時間複雜度,假設每個鏈表的平均元素個數為n,合併1,2鏈表的時候,要遍歷2n個頂點,合併1,2,3鏈表的時候,要遍歷3n個頂點......以此類推。

最後需要遍歷的元素總個數是2n+3n+...+kn,轉化為等差級數求和,可得最後的時間複雜度為 O(n*k^2)

代碼實現如下:

class Solution{ public: ListNode* mergeKLists(vector<ListNode*>& lists) { if(lists.size() == 0) return NULL; ListNode*res = lists[0]; for(int i = 1; i < lists.size(); i++) res = merge2list(res, lists[i]); return res; } ListNode *merge2list(ListNode *head1, ListNode*head2) { ListNode node(0), *res = &node; while(head1 && head2) { if(head1->val <= head2->val) { res->next = head1; head1 = head1->next; } else { res->next = head2; head2 = head2->next; } res = res->next; } if(head1) res->next = head1; else if(head2) res->next = head2; return node.next; }};

交了一發,居然通過了。但是效率確實是排在後邊。

2.分治法:

利用分治的思想,把合併k個鏈表的任務轉化為兩個合併k/2個鏈表的任務,四個合併 k/4個鏈表的任務......直到每次合併的鏈表只剩下1個為止。

代碼實現如下:

class Solution { public: ListNode *mergeKLists(vector<ListNode *> &lists) { int n = lists.size(); if(n == 0)return NULL; while(n >1) { int k = (n+1)/2; for(int i = 0; i < n/2; i++) lists[i] = merge2list(lists[i], lists[i + k]); n = k; } return lists[0]; } ListNode *merge2list(ListNode *head1, ListNode*head2) { ListNode node(0), *res = &node; while(head1 && head2) { if(head1->val <= head2->val) { res->next = head1; head1 = head1->next; } else { res->next = head2; head2 = head2->next; } res = res->next; } if(head1) res->next = head1; else if(head2) res->next = head2; return node.next; } };

這次的時間複雜度還是比較令人滿意的。

3.利用優先隊列:

因為這個題目出現在《演算法導論》的堆排序一章,我們可以考慮一下這題目到底和堆排序有什麼關係。

我只要維護一個k個大小的最小堆,初始化堆中的元素為鏈表的頭節點,每次從堆中選擇最小的元素加入到要返回的鏈表,並且把加入到返回鏈表中的節點所在鏈表的下一個節點加入到堆中。當堆為空時,所有的節點就都被加入到結果鏈表中了。

至於堆的實現,我們可以使用優先隊列。

代碼實現如下:

class Solution {public: struct cmp { bool operator ()(const ListNode *a, const ListNode *b) { return a->val > b->val; } }; ListNode *mergeKLists(vector<ListNode *> &lists) { int n = lists.size(); if(n == 0)return NULL; ListNode node(0), *res = &node; priority_queue<ListNode*, vector<ListNode*>, cmp> que; for(int i = 0; i < n; i++) if(lists[i]) que.push(lists[i]); while(!que.empty()) { ListNode * p = que.top(); que.pop(); res->next = p; res = p; if(p->next) que.push(p->next); } return node.next; }};

但是理論上來說...這個的時間複雜度和分治法是一樣的。可能是常數的鍋吧。

5 後記

《演算法導論》真是一本好書啊!雖然我期末考完試之後就再也沒看過了(逃


推薦閱讀:

[leetcode algorithms]題目21
019 Remove Nth Node From End of List[M]
[leetcode algorithms]題目15
真的智障,真寫不出來,88-Merge Sorted Array
[leetcode algorithms]題目5

TAG:演算法 | LeetCode | 演算法設計 |