(LeetCode-Hot100)23. 合并 K 个升序链表

合并 K 个升序链表(LeetCode 23)

问题简介

🔗 LeetCode 23. 合并 K 个升序链表

题目描述

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。


示例说明

示例 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 = [[]]

输出:
[]


解题思路

✅ 方法一:顺序合并(两两合并)

  • 思路:从第一个链表开始,依次与下一个链表合并。
  • 步骤
    1. 初始化结果链表为 null
    2. 遍历 lists,每次将当前结果与 lists[i] 合并。
    3. 合并两个有序链表使用经典双指针法。

⚠️ 缺点:时间复杂度较高,因为前面的链表会被反复遍历。


✅ 方法二:分治合并(推荐)

  • 思路:采用分治策略,将 K 个链表两两配对合并,直到只剩一个链表。
  • 步骤
    1. 如果 lists 为空,返回 null
    2. 递归地将链表数组分成两半,分别合并。
    3. 合并左右两部分的结果。

💡 类似于归并排序的思想,减少重复比较。


✅ 方法三:优先队列(最小堆)

  • 思路:维护一个大小为 K 的最小堆,每次取出最小节点加入结果。
  • 步骤
    1. 将每个非空链表的头节点加入最小堆。
    2. 弹出堆顶(最小值),加入结果链表。
    3. 若弹出节点有下一个节点,将其加入堆。
    4. 重复直到堆为空。

💡 时间复杂度最优,适合 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. 分成 [ [1,4,5], [1,3,4] ][ [2,6] ]
  2. 左边再分:[1,4,5][1,3,4] 合并 → [1,1,3,4,4,5]
  3. 右边直接是 [2,6]
  4. 最终合并 [1,1,3,4,4,5][2,6][1,1,2,3,4,4,5,6]

答案有效性证明

  • 正确性
    • 所有方法都基于"合并两个有序链表"的正确子程序。
    • 分治和优先队列确保每次取全局最小值,维持升序。
  • 边界处理
    • 空数组、空链表均被正确处理(返回 nullnil)。
  • 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

相关推荐
ab1515172 小时前
2.16完成107、108、111
算法
Moshow郑锴2 小时前
Java SpringBoot 疑难 Bug 排查思路解析:从“语法正确”到“行为相符”
java·spring boot·bug
小O的算法实验室2 小时前
2026年IEEE IOTJ SCI2区TOP,面向关键节点感知的灾害区域无人机集群路径规划,深度解析+性能实测
算法·无人机·论文复现·智能算法·智能算法改进
闻缺陷则喜何志丹2 小时前
【构造】P9215 [入门赛 #11] [yLOI2021] 扶苏与 1 (Hard Version)|普及+
c++·算法·洛谷·构造
APIshop2 小时前
淘宝商品评论接口实战解析:从抓包到数据抓取全链路技术指南
java·python
Neil今天也要学习2 小时前
永磁同步电机控制算法--基于数据驱动的超局部无模型预测电流控制MFPC及改进
单片机·嵌入式硬件·算法
百锦再2 小时前
线程安全的单例模式全方位解读:从原理到最佳实践
java·javascript·安全·spring·单例模式·kafka·tomcat
百锦再3 小时前
Java synchronized关键字详解:从入门到原理(两课时)
java·开发语言·struts·spring·kafka·tomcat·maven
油丶酸萝卜别吃3 小时前
什么是 Java 内存模型(JMM)?
java·开发语言