leetcode題目連接node
爲了簡化分析,咱們設共有k個鏈表,每一個鏈表的最大長度爲n。算法
不斷取出值最小的那個Node(由於每一個list已經排序,因此這一步只須要找出最小的head Node),添加到已排序鏈表的尾部,直到全部lists的全部Node都取完。函數
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { ListNode *_mergeKLists(ListNode *head, ListNode *tail, vector<ListNode *> &lists) { int smallest_node_index = find_smallest_head(lists); // 結束遞歸的狀況 if (smallest_node_index == -1) { tail->next = NULL; return head; } tail->next = lists[smallest_node_index]; lists[smallest_node_index] = lists[smallest_node_index]->next; // 尾遞歸 return _mergeKLists(head, tail->next, lists); } // 從全部鏈表中找到val最小的 headNode int find_smallest_head(vector<ListNode *> &lists) { int smallest_index = -1; for (int i = 0; i < lists.size(); i++) { if (lists[i] == NULL) { lists.erase(lists.begin() + i); // 刪除第i項之後,下輪循環體還要訪問第i項 i--; continue; } if (smallest_index == -1 || lists[i]->val < lists[smallest_index]->val) { smallest_index = i; } } return smallest_index; } public: ListNode *mergeKLists(vector<ListNode *> &lists) { // 經過 dummyHead ,避免對 「head== NULL && tail == NULL」狀況進行額外判斷處理 ListNode *dummyHead = new ListNode(-1); ListNode *res = _mergeKLists(dummyHead, dummyHead, lists)->next; delete dummyHead; return res; } };
每一次遞歸調用_mergeKLists
僅僅是將問題的規模減少1,而不是將問題分解爲多個問題。所以,這能夠被稱爲「減治法」,比「分治法」要慢一些。
這種解法雖然使用了尾遞歸,可是速度依然很慢。緣由有2:code
_mergeKLists
的效果僅僅是解決了1個Node的順序,對問題的簡化程度過小,致使_mergeKLists
須要調用O(nk)次。_mergeKLists
的時間開銷較高。其時間複雜度爲調用find_smallest_head
的時間複雜度,o(k),並且調用vector::erase的耗時較多。綜上所述,這種解法的時間複雜度爲O(nk)*O(k)=O(nk^2)。實際耗時359 ms,僅僅超過9.73%的提交。不是一個好的算法。排序
分治思想:先將lists中的鏈表兩兩合併,而後問題就簡化成了「合併k/2個已排序的鏈表」。遞歸
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { // 加入 merged_head 和 merged_tail 參數,是爲了可以使用尾遞歸 ListNode *merge2Lists(ListNode *list1_head, ListNode *list2_head, ListNode *merged_head, ListNode *merged_tail) { // 兩種結束遞歸的狀況 if (list1_head == NULL) { merged_tail->next = list2_head; return merged_head; } else if (list2_head == NULL) { merged_tail->next = list1_head; return merged_head; } // 須要繼續遞歸的狀況 else if (list1_head->val <= list2_head->val) { merged_tail->next = list1_head; // 尾遞歸 return merge2Lists(list1_head->next, list2_head, merged_head, merged_tail->next); } else { merged_tail->next = list2_head; // 尾遞歸 return merge2Lists(list1_head, list2_head->next, merged_head, merged_tail->next); } } public: ListNode *mergeKLists(vector<ListNode *> &lists) { int lists_num = lists.size(); if (lists_num == 0) return NULL; ListNode *dummyHead = new ListNode(-1); while (lists_num > 1) { for (int i = 0; i < lists_num / 2; i++) { // 經過 dummyHead ,避免對 「merged_head == NULL && merged_tail == NULL」狀況進行額外判斷處理 dummyHead->next = NULL; lists[i] = merge2Lists(lists[i], lists[lists_num - 1 - i], dummyHead, dummyHead)->next; } // 「簡化問題」的過程,就是不斷減半lists_num的過程 lists_num = (lists_num + 1) / 2; } delete dummyHead; // 通過log_2(k)次減半,lists 中只剩下一個sortedList return lists[0]; } };
綜上所述,此解法的時間複雜度爲O(nklogk)。實際耗時26 ms,超過80.34%的提交。相比前面一個解法有巨大提高。ip
在題解1中,find_smallest_head
的時間複雜度等於每次要合併的鏈表數(k),而鏈表數在題解1的算法執行過程當中是基本不變的。所以這個函數的執行時間始終很高,而調用一次這個函數僅僅能幫助咱們選出一個最小的節點(每次選擇的代價高,收益低)。算法的大部分時間都花在這個函數上了。leetcode
而題解2將【合併k個鏈表】分紅k/2個獨立的子問題:合併2個鏈表。獨立的意義是:當我在合併2個鏈表的時候,徹底不須要管其餘的鏈表。這種獨立性使得子問題可以很是高效的解決:每次只須要對比2個節點,就能選出一個節點。雖然每次選出的節點「質量比較差」(這個節點不太多是k個鏈表中最小的那個節點,它僅僅是2個鏈表中最小的那個節點),可是它勝在選擇的成本很是低(僅僅執行一次大小比較)。所以每次的選擇收益相比題解1要低(須要作更屢次的選擇),但代價比題解1要低得多。綜合起來,題解2總的工做量更少。get
其實題解2的思路是與歸併排序是徹底相同的。你能夠仔細對比一下。