题目23 合并K个升序链表
9.1 官方解析
(刚做这道题的时候首先想到的是按顺序依次合并两个链表,但自己写出来的总是超时,跟题解中的顺序合并相比过于繁杂,多了很多没用的步骤。)
下面将使用几种不同的方法来实现合并链表。
解法1:顺序合并
解法2:分治合并(两两合并)
解法3:使用优先队列合并
在看下面几种解法之前,先看一下如何 合并两个有序链表 ?
假设链表 a 和 b 的长度都是 n,要在 O(n) 的时间代价以及 O(1) 的空间代价完成合并。我们的宗旨是 [原地调整链表元素的 next
指针完成合并]。
- 首先需要一个变量
head
来保存合并之后链表的头部,可以将head
设置为一个虚拟的头(方便代码的书写),在整个链表合并完之后,返回它的下一位置。 - 需要一个一直变化的指针
tail
来记录下一插入位置的前一位置,以及两个指针aPtr
和bPtr
记录 a 和 b 未合并部分的第一位。 - 当
aPtr
和bPtr
都不为空时,取val
更小的合并;如果aPtr
为空,把整个bPtr
以及之后的元素全部合并;aPtr
同理。 - 在合并的时候先要调整
tail
的next
属性,再后移tail
和Ptr
。
/*
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
// 合并两个有序链表
ListNode* merge_two_lists(ListNode* a, ListNode* b) {
if (!a || !b) {
return a ? a : b;
}
ListNode head;
ListNode* tail = &head;
ListNode* aPtr = a;
ListNode* bPtr = b;
while (aPtr && bPtr) {
if (aPtr->val < bPtr->val) {
tail->next = aPtr;
aPtr = aPtr->next;
}
else {
tail->next = bPtr;
bPtr = bPtr->next;
}
tail = tail->next;
}
tail->next = (aPtr ? aPtr : bPtr);
return head.next;
}
复杂度分析
- 时间复杂度:O(n)。
- 空间复杂度:O(1)。
9.2 解法1
下面是首先能够想到的最为朴素的解法:顺序合并。
class Solution {
public:
// 合并两个有序链表
ListNode* mergeTwoLists(ListNode *a, ListNode *b) {
if ((!a) || (!b)) return a ? a : b;
ListNode head;
ListNode* tail = &head;
ListNode* aPtr = a;
ListNode* bPtr = b;
while (aPtr && bPtr) {
if (aPtr->val < bPtr->val) {
tail->next = aPtr;
aPtr = aPtr->next;
} else {
tail->next = bPtr;
bPtr = bPtr->next;
}
tail = tail->next;
}
tail->next = (aPtr ? aPtr : bPtr);
return head.next;
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
if (lists.empty()) {
return nullptr;
}
ListNode* result = nullptr;
for (size_t i = 0; i < lists.size(); ++i) {
result = mergeTwoLists(result, lists[i]);
}
return result;
}
};
这种解法内存消耗不高,但是执行用时非常高!
执行用时:170 ms,
内存消耗:12.7 MB
复杂度分析
时间复杂度:O(k²n)。
空间复杂度:O(1)。
9.3 解法2
分治的方法合并,类似于归并排序中的两两合并。(递归!)
class Solution {
public:
// 合并两个有序链表
ListNode* mergeTwoLists(ListNode *a, ListNode *b) {
if ((!a) || (!b)) return a ? a : b;
ListNode head;
ListNode* tail = &head;
ListNode* aPtr = a;
ListNode* bPtr = b;
while (aPtr && bPtr) {
if (aPtr->val < bPtr->val) {
tail->next = aPtr;
aPtr = aPtr->next;
} else {
tail->next = bPtr;
bPtr = bPtr->next;
}
tail = tail->next;
}
tail->next = (aPtr ? aPtr : bPtr);
return head.next;
}
// 递归操作
ListNode* merge(vector<ListNode*>& lists, int low, int high) {
if (low == high) return lists[low];
if (low > high) return nullptr;
// 其实也可以mid = low + (high - low) / 2; 但是下面这种执行更快!
int mid = (low + high) >> 1;
return mergeTwoLists(merge(lists, low, mid), merge(lists, mid + 1, high));
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
return merge(lists, 0, lists.size() - 1);
}
};
执行用时:24 ms,
内存消耗:12.7 MB
9.4 解法3
使用优先队列合并的思路跟前面两种方法有所不同,即把所有的链表存储在一个优先队列中,每次提取当前每个链表没有被合并的元素的最前面一个中最小的那个结点(其实就是提取队首元素,一旦提取就出队列),直到所有链表都被提取完。
由于Comp函数默认是对最大堆进行比较并维持递增关系,如果我们要获取最小的结点值,比较函数就应该维持递减关系,所以operator() 中返回用大于号而不是小于号进行比较。
class Solution {
struct Comp {
bool operator() (ListNode* a, ListNode* b) {
return a->val > b->val;
}
};
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
if (lists.empty()) {
return nullptr;
}
priority_queue<ListNode*, vector<ListNode*>, Comp> q;
for (ListNode* list : lists) {
if (list) {
q.push(list);
}
}
// 也可以ListNode result; ListNode* cur = &result; 最后 return result.next;
ListNode* result = new ListNode(0);
ListNode* cur = result;
while (!q.empty()) {
cur->next = q.top();
q.pop();
cur = cur->next;
if (cur->next) {
q.push(cur->next);
}
}
return result->next;
}
};