问题描述
给定 k 个升序排列的链表,将它们合并成一个新的升序链表。每个链表内部已排序,链表之间无序。
该问题是大规模数据外部排序的核心子过程,在数据库归并操作、日志文件合并、搜索引擎索引合并等场景中频繁出现。
数据结构定义
public class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
// 辅助方法:从数组构建链表
static ListNode fromArray(int[] arr) {
if (arr == null || arr.length == 0) return null;
ListNode dummy = new ListNode(0);
ListNode cur = dummy;
for (int v : arr) {
cur.next = new ListNode(v);
cur = cur.next;
}
return dummy.next;
}
// 辅助方法:打印链表
static void print(ListNode head) {
while (head != null) {
System.out.print(head.val + (head.next != null ? " -> " : ""));
head = head.next;
}
System.out.println();
}
}
解法一:优先队列(最小堆)
维护一个大小为 k 的最小堆,每次取出堆顶最小元素,并将其所在链表的下一个节点入堆。
import java.util.PriorityQueue;
import java.util.Comparator;
public class MergeKListsPQ {
// O(N log K) 时间,O(K) 额外空间,N为总节点数
public static ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) return null;
// 最小堆,按节点值排序
PriorityQueue<ListNode> pq = new PriorityQueue<>(
Comparator.comparingInt(a -> a.val)
);
// 初始化:每个链表的头节点入堆
for (ListNode node : lists) {
if (node != null) pq.offer(node);
}
ListNode dummy = new ListNode(0);
ListNode tail = dummy;
while (!pq.isEmpty()) {
ListNode minNode = pq.poll();
tail.next = minNode;
tail = tail.next;
if (minNode.next != null) {
pq.offer(minNode.next);
}
}
return dummy.next;
}
public static void main(String[] args) {
ListNode[] lists = {
ListNode.fromArray(new int[]{1, 4, 5}),
ListNode.fromArray(new int[]{1, 3, 4}),
ListNode.fromArray(new int[]{2, 6})
};
ListNode result = mergeKLists(lists);
ListNode.print(result); // 1 -> 1 -> 2 -> 3 -> 4 -> 4 -> 5 -> 6
}
}
解法二:分治两两归并
借鉴归并排序思想,将 k 个链表两两配对合并,递归直到只剩一个链表。
public class MergeKListsDivideConquer {
// O(N log K) 时间,O(log K) 递归栈空间
public static ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) return null;
return mergeRange(lists, 0, lists.length - 1);
}
private static ListNode mergeRange(ListNode[] lists, int left, int right) {
if (left == right) return lists[left];
int mid = left + (right - left) / 2;
ListNode l1 = mergeRange(lists, left, mid);
ListNode l2 = mergeRange(lists, mid + 1, right);
return mergeTwoLists(l1, l2);
}
// 合并两个有序链表 O(n)
private static ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
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;
}
public static void main(String[] args) {
ListNode[] lists = {
ListNode.fromArray(new int[]{1, 4, 5}),
ListNode.fromArray(new int[]{1, 3, 4}),
ListNode.fromArray(new int[]{2, 6})
};
ListNode result = mergeKLists(lists);
ListNode.print(result);
}
}
解法三:顺序归并(暴力)
public class MergeKListsSequential {
// O(N * K) 时间,O(1) 额外空间
public static ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) return null;
ListNode result = lists[0];
for (int i = 1; i < lists.length; i++) {
result = mergeTwoLists(result, lists[i]);
}
return result;
}
private static ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
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 × K) | O(1) | 实现简单,效率低 |
| 优先队列 | O(N log K) | O(K) | 直观优雅,适合流式数据 |
| 分治归并 | O(N log K) | O(log K) | 空间更优,无额外数据结构 |
实际应用
在数据库外部排序中,当内存无法容纳全部数据时,会将数据分块排序后产生多个有序”顺串”(run),再用多路归并合并为最终有序结果。合并 K 个有序链表正是这一过程的核心算法。
总结
本文从暴力顺序归并出发,逐步优化到优先队列与分治策略,展示了算法设计中”减少比较次数”与”降低空间开销”的双重优化目标。读者可进一步思考:若要求合并后的链表去重,应如何修改?