LeetCode 第23题:合并K个升序链表

LeetCode 第23题:合并K个升序链表

题目描述

给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。

难度

困难

题目链接

https://leetcode.cn/problems/merge-k-sorted-lists/

示例

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

提示

  • k == lists.length
  • 0 <= k <= 10^4
  • 0 <= lists[i].length <= 500
  • -10^4 <= lists[i][j] <= 10^4
  • lists[i] 按升序排列
  • lists[i].length 的总和不超过 10^4

解题思路

方法一:分治合并

将K个链表的合并问题分解为两两合并的子问题。

关键点:

  1. 使用分治思想,将K个链表分成两部分
  2. 递归地合并每部分的链表
  3. 最后将两个合并后的链表合并
  4. 复用第21题的两个链表合并方法

具体步骤:

  1. 如果链表数组为空,返回null
  2. 如果只有一个链表,直接返回
  3. 将链表数组分成两半
  4. 递归合并左半部分和右半部分
  5. 合并得到的两个链表

时间复杂度:O(N * log k),其中k是链表数量,N是所有节点的总数
空间复杂度:O(log k),递归栈的深度

方法二:优先队列

使用优先队列(最小堆)来维护k个链表的当前最小节点。

思路:

  1. 创建最小堆,将k个链表的头节点加入堆中
  2. 每次从堆中取出最小节点,加入结果链表
  3. 如果被取出的节点有后继节点,将后继节点加入堆中
  4. 重复步骤2-3直到堆为空

代码实现

C# 实现(分治合并)

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     public int val;
 *     public ListNode next;
 *     public ListNode(int val=0, ListNode next=null) {
 *         this.val = val;
 *         this.next = next;
 *     }
 * }
 */
public class Solution {
    public ListNode MergeKLists(ListNode[] lists) {
        if (lists == null || lists.Length == 0) return null;
        return MergeSort(lists, 0, lists.Length - 1);
    }
  
    private ListNode MergeSort(ListNode[] lists, int left, int right) {
        if (left == right) return lists[left];
        if (left > right) return null;
      
        int mid = left + (right - left) / 2;
        ListNode leftList = MergeSort(lists, left, mid);
        ListNode rightList = MergeSort(lists, mid + 1, right);
        return MergeTwoLists(leftList, rightList);
    }
  
    private ListNode MergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0);
        ListNode current = dummy;
      
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                current.next = l1;
                l1 = l1.next;
            } else {
                current.next = l2;
                l2 = l2.next;
            }
            current = current.next;
        }
      
        current.next = l1 ?? l2;
        return dummy.next;
    }
}

C# 实现(优先队列)

public class Solution {
    public ListNode MergeKLists(ListNode[] lists) {
        if (lists == null || lists.Length == 0) return null;
      
        // 创建优先队列,按节点值升序排序
        var pq = new PriorityQueue<ListNode, int>();
      
        // 将所有链表的头节点加入队列
        foreach (var list in lists) {
            if (list != null) {
                pq.Enqueue(list, list.val);
            }
        }
      
        ListNode dummy = new ListNode(0);
        ListNode current = dummy;
      
        // 不断从队列中取出最小节点
        while (pq.Count > 0) {
            ListNode node = pq.Dequeue();
            current.next = node;
            current = current.next;
          
            // 如果取出的节点还有后继节点,将后继节点加入队列
            if (node.next != null) {
                pq.Enqueue(node.next, node.next.val);
            }
        }
      
        return dummy.next;
    }
}

代码详解

分治合并版本:

  1. 分治过程:
    • 将链表数组分成两半
    • 递归合并每一半
    • 最后合并两个有序链表
  2. 合并两个链表:
    • 使用虚拟头节点
    • 比较节点值选择较小的
    • 处理剩余节点

优先队列版本:

  1. 优先队列使用:
    • 按节点值排序
    • 每次取出最小值
    • 动态添加后继节点
  2. 构建结果:
    • 使用虚拟头节点
    • 依次连接最小节点
    • 维护队列状态

执行结果

分治合并版本:

  • 执行用时:92 ms
  • 内存消耗:44.8 MB

优先队列版本:

  • 执行用时:88 ms
  • 内存消耗:45.2 MB

总结与反思

  1. 这是一道综合性的算法题目:
    • 考察分治思想的应用
    • 考察优先队列的使用
    • 考察链表操作的基本功
  2. 两种解法比较:
    • 分治:实现简单,空间效率好
    • 优先队列:时间效率稳定,但需要额外空间
  3. 优化思路:
    • 可以使用迭代而不是递归来实现分治
    • 优先队列可以使用数组实现
    • 考虑链表长度的平衡性

相关题目

posted @   旧厂街小江  阅读(1)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示