【题目】 对于一个链表数组,每个链表都已经按升序排列。请将所有链表合并到一个升序链表中,返回合并后的链表。
示例:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到
1->1->2->3->4->4->5->6
【方法一】 分治合并
思路是用分治的方法进行合并。
将 k 个链表配对并将同一对中的链表合并;
第一轮合并以后, k 个链表被合并成了k/2个链表,平均长度为2n/k,然后是k/4个链表,k/8个链表等;
重复这一过程,直到我们得到了最终的有序链表。
// 用 Java 实现的合并 K 个有序链表的解决方案,采用分治法策略,将问题逐步分解为合并两个有序链表。
class Solution {
// 参数:lists 是一个包含 K 个有序链表头节点的数组。
public ListNode mergeKLists(ListNode[] lists) {
// 调用递归合并方法 merge,初始范围为整个数组(从索引 0 到 lists.length-1)。
return merge(lists, 0, lists.length - 1);
}
// 功能:递归地将链表数组两两合并。
public ListNode merge(ListNode[] lists, int l, int r) {
if (l == r) {
return lists[l];
}
if (l > r) {
return null;
}
// 分治逻辑:计算中间索引 mid(使用右移运算符 >> 1 等同于 (l + r) / 2)。
// 递归合并左半部分(l 到 mid)和右半部分(mid+1 到 r),最后用 mergeTwoLists 合并结果。
int mid = (l + r) >> 1;
return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
}
// 功能:合并两个有序链表 a 和 b。
public ListNode mergeTwoLists(ListNode a, ListNode b) {
// 终止条件:若任一链表为空,直接返回另一个链表。
if (a == null || b == null) {
return a != null ? a : b;
}
// 虚拟头节点:创建 head 作为虚拟头节点,便于操作。
ListNode head = new ListNode(0);
// 双指针遍历:用 aPtr 和 bPtr 分别遍历两个链表,tail 指向当前合并链表的末尾。
ListNode tail = head, aPtr = a, bPtr = b;
while (aPtr != null && bPtr != null) {
// 比较与连接:每次比较 aPtr 和 bPtr 的值,将较小节点连接到 tail 后,并移动相应指针。
if (aPtr.val < bPtr.val) {
tail.next = aPtr;
aPtr = aPtr.next;
} else {
tail.next = bPtr;
bPtr = bPtr.next;
}
tail = tail.next;
}
// 处理剩余节点:遍历结束后,将非空链表的剩余部分直接连接到 tail。
tail.next = (aPtr != null ? aPtr : bPtr);
// 返回结果:head.next 即为合并后的真实头节点。
return head.next;
}
}
复杂度分析
时间复杂度:考虑递归「向上回升」的过程——第一轮合并k/2组链表,每一组的时间代价是 O(2n);第二轮合并k/4组链表,每一组的时间代价是 O(4n)......
所以总的时间代价是
∞
O(∑ k/2^i * 2^i * n
i=1 )
= O(kn×logk),故渐进时间复杂度为 O(kn×logk)。
空间复杂度:递归会使用到 O(logk) 空间代价的栈空间。
用C++代码实现如下:
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);
}
};
【方法二】 使用优先队列合并
class Solution {
// 定义内部类Status,封装链表节点的值和指针
// 实现Comparable接口以便在优先队列中排序
class Status implements Comparable<Status> {
int val; // 链表节点的值
ListNode ptr; // 链表节点的指针
// 构造函数,初始化节点值和指针
Status(int val, ListNode ptr) {
this.val = val;
this.ptr = ptr;
}
// 实现比较方法,定义排序规则(按节点值升序)
public int compareTo(Status status2) {
return this.val - status2.val;
}
}
// 创建优先队列(最小堆),用于动态维护当前最小值节点
PriorityQueue<Status> queue = new PriorityQueue<Status>();
// 合并K个有序链表的主方法
public ListNode mergeKLists(ListNode[] lists) {
// 遍历所有链表,将每个链表的头节点(如果不为空)加入优先队列
for (ListNode node: lists) {
if (node != null) {
queue.offer(new Status(node.val, node));
}
}
// 创建虚拟头节点,简化链表操作
ListNode head = new ListNode(0);
// tail指针指向当前合并链表的末尾节点
ListNode tail = head;
// 循环处理优先队列,直到队列为空
while (!queue.isEmpty()) {
// 取出队列中当前最小的节点
Status f = queue.poll();
// 将该节点连接到结果链表的尾部
tail.next = f.ptr;
// 更新tail指针到新连接的节点
tail = tail.next;
// 如果该节点有下一个节点,将下一个节点加入队列
if (f.ptr.next != null) {
queue.offer(new Status(f.ptr.next.val, f.ptr.next));
}
}
// 返回虚拟头节点的下一个节点,即合并后链表的头节点
return head.next;
}
}
对于上面的代码,其核心逻辑详解如下:
1)Status 类:
封装链表节点的值和指针。compareTo方法定义了比较规则,使优先队列能按节点值升序排列元素。
2)优先队列初始化:
PriorityQueue queue = new PriorityQueue();
创建最小堆,默认按自然顺序(即 Status 类定义的比较规则)排序。
3)初始入队操作:
for (ListNode node: lists) {
if (node != null) {
queue.offer(new Status(node.val, node));
}
}
将每个非空链表的头节点加入队列,队列初始状态包含 K 个链表的头节点,按节点值排序。
4)合并主循环:
while (!queue.isEmpty()) {
Status f = queue.poll(); // 取出当前最小节点
tail.next = f.ptr; // 连接到结果链表
tail = tail.next; // 更新尾指针
if (f.ptr.next != null) { // 如果有后续节点
queue.offer(new Status(f.ptr.next.val, f.ptr.next)); // 后续节点入队
}
}
每次循环从队列取出最小值节点,加入结果链表;若该节点有后续节点,将后续节点入队,队列自动调整顺序;循环结束时,所有节点按值升序加入结果链表。
5)虚拟头节点的使用:
ListNode head = new ListNode(0);
ListNode tail = head;
// ...
return head.next;
虚拟头节点简化了链表操作,避免处理空链表的边界情况;最终返回head.next即为合并后的真实头节点。
6)复杂度分析
时间复杂度:考虑优先队列中的元素不超过 k 个,那么插入和删除的时间代价为 O(logk),这里最多有 kn 个点,对于每个点都被插入删除各一次,故总的时间代价即渐进时间复杂度为 O(kn×logk)。
空间复杂度:这里用了优先队列,优先队列中的元素不超过 k 个,故渐进空间复杂度为 O(k)。
另附:上面的代码用到了PriorityQueue。
PriorityQueue 是 Java 集合框架中的一个重要类,它实现了优先队列的功能。
优先队列是一种特殊的队列,元素出队的顺序由元素的优先级决定,而非元素入队的顺序。
PriorityQueue 类概述
类定义:public class PriorityQueue extends AbstractQueue implements Serializable
实现接口:Queue, Collection, Iterable
数据结构:基于最小堆实现(通过数组表示的完全二叉树)
元素顺序:默认按元素的自然顺序排序(元素需实现 Comparable 接口),也可通过构造函数传入 Comparator 自定义排序规则。
容量特性:动态扩容,初始容量为 11,当元素数量超过容量时自动增长。
PriorityQueue 关键特性
1)排序规则
自然顺序:元素必须实现 Comparable 接口,否则抛出 ClassCastException
自定义顺序:通过构造函数传入 Comparator
// 示例:创建降序排列的优先队列
PriorityQueue maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
2)堆操作原理
插入操作(offer/add):将元素添加到数组末尾;向上调整堆(siftUp),确保堆性质。
删除操作(poll/remove):移除数组首元素;将数组末尾元素移至首位;向下调整堆(siftDown),确保堆性质。
3)线程安全性
PriorityQueue 是非线程安全的,线程安全场景需使用 PriorityBlockingQueue
// 线程安全的优先队列
PriorityBlockingQueue blockingQueue = new PriorityBlockingQueue<>();
4)迭代器行为
iterator() 返回的迭代器不保证元素有序
若需有序遍历,可将元素倒入数组并排序:
Object[] sortedElements = queue.toArray();
Arrays.sort(sortedElements)
用C++代码实现的方案:
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;
}
};