合并 K 个升序链表(LeetCode 23)
问题简介
题目描述
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例说明
示例 1:
输入:
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。
示例 2:
输入:
lists = []
输出:
[]
示例 3:
输入:
lists = [[]]
输出:
[]
解题思路
✅ 方法一:顺序合并(两两合并)
- 思路:从第一个链表开始,依次与下一个链表合并。
- 步骤 :
- 初始化结果链表为
null。 - 遍历
lists,每次将当前结果与lists[i]合并。 - 合并两个有序链表使用经典双指针法。
- 初始化结果链表为
⚠️ 缺点:时间复杂度较高,因为前面的链表会被反复遍历。
✅ 方法二:分治合并(推荐)
- 思路:采用分治策略,将 K 个链表两两配对合并,直到只剩一个链表。
- 步骤 :
- 如果
lists为空,返回null。 - 递归地将链表数组分成两半,分别合并。
- 合并左右两部分的结果。
- 如果
💡 类似于归并排序的思想,减少重复比较。
✅ 方法三:优先队列(最小堆)
- 思路:维护一个大小为 K 的最小堆,每次取出最小节点加入结果。
- 步骤 :
- 将每个非空链表的头节点加入最小堆。
- 弹出堆顶(最小值),加入结果链表。
- 若弹出节点有下一个节点,将其加入堆。
- 重复直到堆为空。
💡 时间复杂度最优,适合 K 较大的情况。
代码实现
java:Java
// 方法一:顺序合并
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
ListNode res = null;
for (ListNode list : lists) {
res = mergeTwoLists(res, list);
}
return res;
}
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) return l2;
if (l2 == null) return l1;
if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
// 方法二:分治合并
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 l, int r) {
if (l == r) return lists[l];
int mid = l + (r - l) / 2;
ListNode left = merge(lists, l, mid);
ListNode right = merge(lists, mid + 1, r);
return mergeTwoLists(left, right);
}
private 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;
}
}
// 方法三:优先队列(最小堆)
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) return null;
PriorityQueue<ListNode> pq = new PriorityQueue<>((a, b) -> a.val - b.val);
for (ListNode node : lists) {
if (node != null) pq.offer(node);
}
ListNode dummy = new ListNode(0);
ListNode cur = dummy;
while (!pq.isEmpty()) {
ListNode node = pq.poll();
cur.next = node;
cur = cur.next;
if (node.next != null) {
pq.offer(node.next);
}
}
return dummy.next;
}
}
go:Go
// 方法一:顺序合并
func mergeKLists(lists []*ListNode) *ListNode {
var res *ListNode
for _, list := range lists {
res = mergeTwoLists(res, list)
}
return res
}
func mergeTwoLists(l1, l2 *ListNode) *ListNode {
if l1 == nil {
return l2
}
if l2 == nil {
return l1
}
if l1.Val < l2.Val {
l1.Next = mergeTwoLists(l1.Next, l2)
return l1
} else {
l2.Next = mergeTwoLists(l1, l2.Next)
return l2
}
}
// 方法二:分治合并
func mergeKLists(lists []*ListNode) *ListNode {
if len(lists) == 0 {
return nil
}
return merge(lists, 0, len(lists)-1)
}
func merge(lists []*ListNode, l, r int) *ListNode {
if l == r {
return lists[l]
}
mid := l + (r-l)/2
left := merge(lists, l, mid)
right := merge(lists, mid+1, r)
return mergeTwoLists(left, right)
}
func mergeTwoLists(l1, l2 *ListNode) *ListNode {
dummy := &ListNode{}
cur := dummy
for l1 != nil && l2 != nil {
if l1.Val < l2.Val {
cur.Next = l1
l1 = l1.Next
} else {
cur.Next = l2
l2 = l2.Next
}
cur = cur.Next
}
if l1 != nil {
cur.Next = l1
} else {
cur.Next = l2
}
return dummy.Next
}
// 方法三:优先队列(最小堆)
import "container/heap"
type minHeap []*ListNode
func (h minHeap) Len() int { return len(h) }
func (h minHeap) Less(i, j int) bool { return h[i].Val < h[j].Val }
func (h minHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *minHeap) Push(x interface{}) { *h = append(*h, x.(*ListNode)) }
func (h *minHeap) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
func mergeKLists(lists []*ListNode) *ListNode {
if len(lists) == 0 {
return nil
}
h := &minHeap{}
heap.Init(h)
for _, node := range lists {
if node != nil {
heap.Push(h, node)
}
}
dummy := &ListNode{}
cur := dummy
for h.Len() > 0 {
node := heap.Pop(h).(*ListNode)
cur.Next = node
cur = cur.Next
if node.Next != nil {
heap.Push(h, node.Next)
}
}
return dummy.Next
}
示例演示
以 lists = [[1,4,5],[1,3,4],[2,6]] 为例,使用分治法:
- 分成
[ [1,4,5], [1,3,4] ]和[ [2,6] ] - 左边再分:
[1,4,5]与[1,3,4]合并 →[1,1,3,4,4,5] - 右边直接是
[2,6] - 最终合并
[1,1,3,4,4,5]与[2,6]→[1,1,2,3,4,4,5,6]
答案有效性证明
- 正确性 :
- 所有方法都基于"合并两个有序链表"的正确子程序。
- 分治和优先队列确保每次取全局最小值,维持升序。
- 边界处理 :
- 空数组、空链表均被正确处理(返回
null或nil)。
- 空数组、空链表均被正确处理(返回
- LeetCode 测试通过:三种方法均可 AC。
复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 顺序合并 | O ( k N ) O(kN) O(kN) | O ( 1 ) O(1) O(1) | N N N 为平均链表长度,最坏需合并 k k k 次,每次 O ( N ) O(N) O(N) |
| 分治合并 | O ( N l o g k ) O(N \\log k) O(Nlogk) | O ( l o g k ) O(\\log k) O(logk) | 递归深度 l o g k \\log k logk,每层总合并代价 O ( N ) O(N) O(N) |
| 优先队列 | O ( N l o g k ) O(N \\log k) O(Nlogk) | O ( k ) O(k) O(k) | 每个节点入堆一次,堆操作 O ( l o g k ) O(\\log k) O(logk) |
✅ 推荐使用分治或优先队列 ,尤其当 k k k 较大时。
问题总结
- 核心思想:将多路归并转化为两两归并或利用堆结构高效取最小值。
- 关键技巧 :
- 分治降低重复计算;
- 最小堆动态维护当前候选最小值。
- 适用场景 :
- 外部排序(如大数据归并);
- 多路日志合并、事件流处理等。
github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions