LeetCode 23. 合并 K 个升序链表
问题描述
给定一个包含 k
个升序链表的数组,将这些链表合并为一个新的升序链表并返回。
示例:
输入: lists = [[1,4,5],[1,3,4],[2,6]]
输出: [1,1,2,3,4,4,5,6]
算法思路
方法一:最小堆(优先队列)
- 核心思想:
- 使用最小堆维护当前所有链表头节点
- 每次取出最小节点加入结果链表
- 将该节点的下一个节点加入堆中
- 步骤:
- 初始化:将所有非空链表头节点入堆
- 循环处理:
- 弹出堆顶节点(当前最小值)
- 链接到结果链表
- 若该节点有后继,则后继入堆
- 时间复杂度:O(N log k)
- N:总节点数
- k:链表个数
方法二:分治合并
- 核心思想:
- 两两合并链表(类似归并排序)
- 递归合并直到只剩一个链表
- 步骤:
- 递归基:链表数为0或1时直接返回
- 分割:将链表数组分为两半
- 递归合并左右两部分
- 合并两个有序链表
- 时间复杂度:O(N log k)
- 每层合并操作 O(N)
- 递归深度 O(log k)
代码实现
方法一:最小堆(优先队列)
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) return null;
// 创建最小堆(优先队列)
PriorityQueue<ListNode> minHeap = new PriorityQueue<>((a, b) -> a.val - b.val);
// 初始化:将所有非空链表头节点入堆
for (ListNode node : lists) {
if (node != null) {
minHeap.offer(node);
}
}
// 创建哑节点作为结果链表头
ListNode dummy = new ListNode(-1);
ListNode current = dummy;
// 循环处理堆中节点
while (!minHeap.isEmpty()) {
// 弹出当前最小节点
ListNode minNode = minHeap.poll();
// 链接到结果链表
current.next = minNode;
current = current.next;
// 若该节点有后继,则后继入堆
if (minNode.next != null) {
minHeap.offer(minNode.next);
}
}
return dummy.next;
}
}
方法二:分治合并
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) return null;
return merge(lists, 0, lists.length - 1);
}
// 分治合并主函数
private ListNode merge(ListNode[] lists, int left, int right) {
// 递归基:单个链表直接返回
if (left == right) return lists[left];
// 计算中间位置
int mid = left + (right - left) / 2;
// 递归合并左右两部分
ListNode l1 = merge(lists, left, mid);
ListNode l2 = merge(lists, mid + 1, right);
// 合并两个有序链表
return mergeTwoLists(l1, l2);
}
// 合并两个有序链表(迭代法)
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(-1);
ListNode cur = dummy;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
cur.next = l1;
l1 = l1.next;
} else {
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
// 链接剩余部分
cur.next = (l1 != null) ? l1 : l2;
return dummy.next;
}
}
算法分析
- 时间复杂度:
- 最小堆:O(N log k),每个节点入堆出堆一次
- 分治合并:O(N log k),每层合并操作 O(N),共 O(log k) 层
- 空间复杂度:
- 最小堆:O(k),堆中最多存储 k 个节点
- 分治合并:O(log k),递归调用栈深度
算法过程(最小堆法)
lists = [[1,4,5],[1,3,4],[2,6]]
:
- 初始化堆:入堆 [1,1,2]
- 循环处理:
- 弹出1(第一个链表)→ 结果:1 → 入堆4
- 弹出1(第二个链表)→ 结果:1→1 → 入堆3
- 弹出2 → 结果:1→1→2 → 入堆6
- 弹出3 → 结果:1→1→2→3 → 入堆4
- 弹出4 → 结果:1→1→2→3→4 → 入堆5
- 弹出4 → 结果:1→1→2→3→4→4
- 弹出5 → 结果:1→1→2→3→4→4→5
- 弹出6 → 结果:1→1→2→3→4→4→5→6
- 返回结果:[1,1,2,3,4,4,5,6]
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
ListNode l1 = new ListNode(1, new ListNode(4, new ListNode(5)));
ListNode l2 = new ListNode(1, new ListNode(3, new ListNode(4)));
ListNode l3 = new ListNode(2, new ListNode(6));
ListNode[] lists1 = {l1, l2, l3};
printList(solution.mergeKLists(lists1)); // 1->1->2->3->4->4->5->6
// 测试用例2:空链表数组
ListNode[] lists2 = {};
printList(solution.mergeKLists(lists2)); // null
// 测试用例3:包含空链表
ListNode[] lists3 = {null, new ListNode(1)};
printList(solution.mergeKLists(lists3)); // 1
// 测试用例4:单个链表
ListNode l4 = new ListNode(0, new ListNode(2, new ListNode(5)));
ListNode[] lists4 = {l4};
printList(solution.mergeKLists(lists4)); // 0->2->5
// 测试用例5:全空链表
ListNode[] lists5 = {null, null};
printList(solution.mergeKLists(lists5)); // null
}
// 辅助方法:打印链表
private static void printList(ListNode head) {
if (head == null) {
System.out.println("null");
return;
}
StringBuilder sb = new StringBuilder();
while (head != null) {
sb.append(head.val);
if (head.next != null) sb.append("->");
head = head.next;
}
System.out.println(sb);
}
关键点
-
最小堆选择:
- 优先队列实现最小堆
- 自定义比较器:
(a, b) -> a.val - b.val
-
分治合并要点:
- 递归分割数组
- 合并两个有序链表(基础操作)
-
边界处理:
- 空链表数组直接返回 null
- 链表为空时不入堆/不参与合并
常见问题
-
为什么最小堆大小为 O(k)?
- 堆中最多存储每个链表的一个节点,共 k 个
-
分治合并的递归深度是多少?
- O(log k),每次将问题规模减半
-
哪种方法更优?
- 最小堆:代码简洁,适合链表长度差异大的场景
- 分治合并:空间效率更高(无额外堆空间),适合大规模数据
-
如何处理链表中的空节点?
- 入堆前检查非空,合并时忽略空链表