算法题 合并 K 个升序链表

LeetCode 23. 合并 K 个升序链表

问题描述

给定一个包含 k 个升序链表的数组,将这些链表合并为一个新的升序链表并返回。

示例

输入: lists = [[1,4,5],[1,3,4],[2,6]]
输出: [1,1,2,3,4,4,5,6]

算法思路

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

  1. 核心思想
    • 使用最小堆维护当前所有链表头节点
    • 每次取出最小节点加入结果链表
    • 将该节点的下一个节点加入堆中
  2. 步骤
    • 初始化:将所有非空链表头节点入堆
    • 循环处理:
      • 弹出堆顶节点(当前最小值)
      • 链接到结果链表
      • 若该节点有后继,则后继入堆
  3. 时间复杂度:O(N log k)
    • N:总节点数
    • k:链表个数

方法二:分治合并

  1. 核心思想
    • 两两合并链表(类似归并排序)
    • 递归合并直到只剩一个链表
  2. 步骤
    • 递归基:链表数为0或1时直接返回
    • 分割:将链表数组分为两半
    • 递归合并左右两部分
    • 合并两个有序链表
  3. 时间复杂度: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,1,2]
  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
  3. 返回结果:[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);
}

关键点

  1. 最小堆选择

    • 优先队列实现最小堆
    • 自定义比较器:(a, b) -> a.val - b.val
  2. 分治合并要点

    • 递归分割数组
    • 合并两个有序链表(基础操作)
  3. 边界处理

    • 空链表数组直接返回 null
    • 链表为空时不入堆/不参与合并

常见问题

  1. 为什么最小堆大小为 O(k)?

    • 堆中最多存储每个链表的一个节点,共 k 个
  2. 分治合并的递归深度是多少?

    • O(log k),每次将问题规模减半
  3. 哪种方法更优?

    • 最小堆:代码简洁,适合链表长度差异大的场景
    • 分治合并:空间效率更高(无额外堆空间),适合大规模数据
  4. 如何处理链表中的空节点?

    • 入堆前检查非空,合并时忽略空链表
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值