LeetCode—23.合并K个排序链表[Merge k Sorted Lists]——分析及代码[C++]
一、题目
合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
示例:
输入:
[
1->4->5,
1->3->4,
2->6
]
输出: 1->1->2->3->4->4->5->6
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/merge-k-sorted-lists
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
二、分析及代码
1. 每次取出最小的头部节点
(1)思路
在各排序链表中最小的一定是头部节点,因此所有链表中最小的节点也一定位于题目所给的头部节点向量中,即 vector<ListNode*> lists 中的最小节点。
因此考虑依次取出 lists中的最小节点,组成答案链表。
由于每次都要遍历寻找 lists 中最小节点,时间复杂度为 O(kn),有待优化。
(2)代码
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
ListNode head(0);
return merge(lists, &head);
}
ListNode* merge(vector<ListNode*>& lists, ListNode* head) {
bool e = true;
int index = -1;
int m = INT_MAX;
for (int i = 0; i < lists.size(); i++) {
if (lists[i] != NULL && lists[i]->val < m) {
index = i;
m = lists[i]->val;
e = false;
}
}
if (e) return NULL;
head->next = lists[index];
head = head->next;
lists[index] = lists[index]->next;
head->next = merge(lists, head);
return head;
}
};
(3)结果
执行用时 :536 ms, 在所有 C++ 提交中击败了 12.61% 的用户;
内存消耗 :11.3 MB。
2. 优先队列寻找最小节点
(1)思路
上述方法每次都要重复寻找 lists 的 k 个节点中的最小节点,过程存在较大优化空间。
考虑使用 map 存储节点的值和下标,但 map 存在难以处理重复数据的问题。
考虑使用优先队列(priority_queue)对待判断的节点进行存储,每次取出最小值,然后将其next节点(如果存在)存入队列中。
由于每次 priority_queue 复杂度为O(logn),此时整体时间复杂度为 O(nlogk)。
(2)代码
class Solution {
public:
struct cmp {
bool operator () (ListNode* a, ListNode* b) {
return (a->val > b->val);
}
};
ListNode* mergeKLists(vector<ListNode*>& lists) {
ListNode prehead(0);
ListNode* head = &prehead;
priority_queue<ListNode*, vector<ListNode*>, cmp> nums;
for (int i = 0; i < lists.size(); i++) {
if (lists[i] != NULL) {
nums.push(lists[i]);
}
}
while (!nums.empty()) {
head->next = nums.top();
head = head->next;
nums.pop();
if (head->next != NULL)
nums.push(head->next);
}
return prehead.next;
}
};
(3)结果
执行用时 :32 ms, 在所有 C++ 提交中击败了 94.82% 的用户;
内存消耗 :10.9 MB。
3. 二分+归并
(1)思路
结合第21题 “合并两个有序链表” 的方法,对 k 个链表通过二分+归并的方法,两两合并直至合为一个链表。
此时一共要合并 logk 次,每次都对 n 个节点进行合并,时间复杂度为 O(nlogk)。
(2)代码
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {//这部分为搬运的21题代码
if (l1 == NULL)
return l2;
if (l2 == NULL)
return l1;
if (l1->val >= l2->val) {
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
else {
l1->next = mergeTwoLists(l1->next, l2);
return l1;
}
}
ListNode* partition(vector<ListNode*>& lists, int l, int r) {
switch (r - l) {
case 0:
return lists[l];
case 1:
return mergeTwoLists(lists[l], lists[r]);
default:
int mid = (r + l) / 2;
return mergeTwoLists(partition(lists, l, mid), partition(lists, mid + 1, r));
}
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
if (lists.size() == 0)
return NULL;
return partition(lists, 0, lists.size() - 1);
}
};
(3)结果
执行用时 :24 ms, 在所有 C++ 提交中击败了 99.73% 的用户;
内存消耗 :11.2 MB。
三、其他
方法1 的 debug 经历了一个曲折的过程。
第1个bug:
只要输出结果不为空,任何简单的例子都会“超出时间限制”。
debug:
在vs、dev和playground中反复测试,输出答案均无异常,且playground在简单例子下运行时间为0-4ms,无论如何不应“超出时间限制”。
一句句加注释反复尝试,发现可能问题出在答案判定过程,由于判定过程存在bug陷入循环导致时间超出。
梳理代码思路,终于发现
if (e) return head;
这句导致链表尾部的 next 指针指向了自己,陷入了死循环,导致判定过程超时。
改为
if (e) return NULL;
bug解除……
第2个bug:
运行后显示“超出内存限制”。
debug:
发现函数入参部分写成了值传递,导致每次都复制了一遍 lists
ListNode* merge(vector<ListNode*> lists, ListNode* head)
改为
ListNode* merge(vector<ListNode*>& lists, ListNode* head)
bug解除。