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的思路是与归并排序是彻底相同的。你能够仔细对比一下。