23. 合并K个排序链表
难度困难835收藏分享切换为英文关注反馈
合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
示例:
输入:
[
1->4->5,
1->3->4,
2->6
]
输出: 1->1->2->3->4->4->5->6
c++实现
文字题解
前置知识:合并两个有序链表
思路
在解决「合并K个排序链表」这个问题之前,我们先来看一个更简单的问题:如何合并两个有序链表?假设链表 a 和 b 的长度都是 n,如何在 O(n)时间代价以及 O(1)的空间代价完成合并? 这个问题在面试中常常出现,为了达到空间代价是 O(1),我们的宗旨是「原地调整链表元素的 next
指针完成合并」。以下是合并的步骤和注意事项,对这个问题比较熟悉的读者可以跳过这一部分。此部分建议结合代码阅读。
- 首先我们需要一个变量
head
来保存合并之后链表的头部,你可以把head
设置为一个虚拟的头(也就是head
的val
属性不保存任何值),这是为了方便代码的书写,在整个链表合并完之后,返回它的下一位置即可。 - 我们需要一个指针
tail
来记录下一个插入位置的前一个位置,以及两个指针aPtr
和bPtr
来记录 aa 和 bb 未合并部分的第一位。注意这里的描述,tail
不是下一个插入的位置,aPtr
和bPtr
所指向的元素处于「待合并」的状态,也就是说它们还没有合并入最终的链表。 当然你也可以给他们赋予其他的定义,但是定义不同实现就会不同。 - 当
aPtr
和bPtr
都不为空的时候,取val
熟悉较小的合并;如果aPtr
为空,则把整个bPtr
以及后面的元素全部合并;bPtr
为空时同理。 - 在合并的时候,应该先调整
tail
的next
属性,再后移tail
和*Ptr
(aPtr
或者bPtr
)。那么这里tail
和*Ptr
是否存在先后顺序呢?它们谁先动谁后动都是一样的,不会改变任何元素的next
指针。
代码
ListNode* mergeTwoLists(ListNode *a, ListNode *b) {
if ((!a) || (!b)) return a ? a : b;
ListNode head, *tail = &head, *aPtr = a, *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)。
方法一:顺序合并
思路
我们可以想到一种最朴素的方法:用一个变量 ans
来维护以及合并的链表,第 i次循环把第 i 个链表和 ans
合并,答案保存到 ans
中。
代码
class Solution {
public:
ListNode* mergeTwoLists(ListNode *a, ListNode *b) {
if ((!a) || (!b)) return a ? a : b;
ListNode head, *tail = &head, *aPtr = a, *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) {
ListNode *ans = nullptr;
for (size_t i = 0; i < lists.size(); ++i) {
ans = mergeTwoLists(ans, lists[i]);
}
return ans;
}
};
方法二:分治合并
思路
考虑优化方法一,用分治的方法进行合并。
- 将 k 个链表配对并将同一对中的链表合并;
- 第一轮合并以后, k 个链表被合并成了 k 2 \frac{k}{2} 2k个链表,平均长度为 2 n k \frac{2n}{k} k2n,然后是 k 4 \frac{k}{4} 4k 个链表, k 8 \frac{k}{8} 8k 个链表等等;
- 重复这一过程,直到我们得到了最终的有序链表。
代码
class Solution {
public:
ListNode* mergeTwoLists(ListNode *a, ListNode *b) {
if ((!a) || (!b)) return a ? a : b;
ListNode head, *tail = &head, *aPtr = a, *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 l, int r) {
if (l == r) return lists[l];
if (l > r) return nullptr;
int mid = (l + r) >> 1;
return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
return merge(lists, 0, lists.size() - 1);
}
};
方法三:使用优先队列合并
思路
这个方法和前两种方法的思路有所不同,我们需要维护当前每个链表没有被合并的元素的最前面一个,k个链表就最多有 k个满足这样条件的元素,每次在这些元素里面选取 val
属性最小的元素合并到答案中。在选取最小元素的时候,我们可以用优先队列来优化这个过程。
代码
class Solution {
public:
struct Status {
int val;
ListNode *ptr;
bool operator < (const Status &rhs) const {
return val > rhs.val;
}
};
priority_queue <Status> q;
ListNode* mergeKLists(vector<ListNode*>& lists) {
for (auto node: lists) {
if (node) q.push({node->val, node});
}
ListNode head, *tail = &head;
while (!q.empty()) {
auto f = q.top(); q.pop();
tail->next = f.ptr;
tail = tail->next;
if (f.ptr->next) q.push({f.ptr->next->val, f.ptr->next});
}
return head.next;
}
};
Java实现
一、K 指针:K 个指针分别指向 K 条链表
1. 每次 O(K)O(K) 比较 K个指针求 min, 时间复杂度:O(NK)
- Java
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
int k = lists.length;
ListNode dummyHead = new ListNode(0);
ListNode tail = dummyHead;
while (true) {
ListNode minNode = null;
int minPointer = -1;
for (int i = 0; i < k; i++) {
if (lists[i] == null) {
continue;
}
if (minNode == null || lists[i].val < minNode.val) {
minNode = lists[i];
minPointer = i;
}
}
if (minPointer == -1) {
break;
}
tail.next = minNode;
tail = tail.next;
lists[minPointer] = lists[minPointer].next;
}
return dummyHead.next;
}
}
2. 使用小根堆对 1 进行优化,每次 O(logK)比较 K个指针求 min, 时间复杂度:O(NlogK)
- Java
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
Queue<ListNode> pq = new PriorityQueue<>((v1, v2) -> v1.val - v2.val);
for (ListNode node: lists) {
if (node != null) {
pq.offer(node);
}
}
ListNode dummyHead = new ListNode(0);
ListNode tail = dummyHead;
while (!pq.isEmpty()) {
ListNode minNode = pq.poll();
tail.next = minNode;
tail = minNode;
if (minNode.next != null) {
pq.offer(minNode.next);
}
}
return dummyHead.next;
}
}
二、 逐一合并两条链表
首先复习一下 「21. 合并两个有序链表」
下面分别贴出「merge2Lists」的「递归」 和 「迭代」两种实现 :
合并两条有序链表 — 递归
- Java
private ListNode merge2Lists(ListNode l1, ListNode l2) {
if (l1 == null) {
return l2;
}
if (l2 == null) {
return l1;
}
if (l1.val < l2.val) {
l1.next = merge2Lists(l1.next, l2);
return l1;
}
l2.next = merge2Lists(l1, l2.next);
return l2;
}
合并两条有序链表 — 迭代
- Java
private ListNode merge2Lists(ListNode l1, ListNode l2) {
ListNode dummyHead = new ListNode(0);
ListNode tail = dummyHead;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
tail.next = l1;
l1 = l1.next;
} else {
tail.next = l2;
l2 = l2.next;
}
tail = tail.next;
}
tail.next = l1 == null? l2: l1;
return dummyHead.next;
}
1. 逐一合并两条链表, 时间复杂度:O(NK)
- Java
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
ListNode res = null;
for (ListNode list: lists) {
res = merge2Lists(res, list);
}
return res;
}
}
2. 两两合并对 1 进行优化,时间复杂度:O(NlogK)
时间复杂度分析:K 条链表的总结点数是 N,平均每条链表有 N/K个节点,因此合并两条链表的时间复杂度是 O(N/K)。从 K条链表开始两两合并成 1条链表,因此每条链表都会被合并 logK次,因此 K 条链表会被合并 K * logK次,因此总共的时间复杂度是 K ∗ l o g K ∗ N / K K*logK*N/K K∗logK∗N/K 即 O(NlogK)。
下面分别贴出「两两合并」的「递归」 和 「迭代」两种实现 :
两两合并 - 迭代
- Java
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if (lists.length == 0) {
return null;
}
int k = lists.length;
while (k > 1) {
int idx = 0;
for (int i = 0; i < k; i += 2) {
if (i == k - 1) {
lists[idx++] = lists[i];
} else {
lists[idx++] = merge2Lists(lists[i], lists[i + 1]);
}
}
k = idx;
}
return lists[0];
}
}
两两合并 - 递归
- Java
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if (lists.length == 0) {
return null;
}
return merge(lists, 0, lists.length - 1);
}
private ListNode merge(ListNode[] lists, int lo, int hi) {
if (lo == hi) {
return lists[lo];
}
int mid = lo + (hi - lo) / 2;
ListNode l1 = merge(lists, lo, mid);
ListNode l2 = merge(lists, mid + 1, hi);
return merge2Lists(l1, l2);
}
}
三、总结
掌握两种 O(NlogK) 方法:
- K 指针指向 K 条链表,每次使用小根堆来 logK 求出最小值;
- 对 K 条链表进行两两合并(递归 / 迭代)。