【力扣-23. 合并k个升序链表】Python笔记

0 阅读4分钟

告别暴力合并!用“分治法”优雅解决K个链表合并

摘要:本文详解 LeetCode 23 题“合并 K 个升序链表”。通过“分治法”将复杂问题拆解为简单的“两两合并”,结合归并排序思想,将时间复杂度优化至 O(NlogK)O(N \log K),带你掌握处理多路归并的高级技巧。


📚 核心知识点:分治法与多路归并

在处理“合并 K 个有序序列”这类问题时,我们通常有两种思路:

  1. 暴力法(顺序合并) :先合并前两个,再用结果合并第三个……以此类推。

    • 缺点:时间复杂度高,因为参与合并的链表会越来越长。
  2. 优先队列(最小堆) :维护一个大小为 K 的小顶堆,每次取出最小的节点。

    • 优点:效率极高。
  3. 分治法(Divide and Conquer) :这就是我们要讲的解法!

    • 核心思想“大事化小” 。既然合并 2 个链表你会写(LeetCode 21题),那合并 K 个链表其实就是把 K 个链表两两配对,合并成 K/2 个,再配对……直到最后剩 1 个。
    • 优势:逻辑清晰,代码复用性高(直接复用“合并两个有序链表”的代码),且时间复杂度优于暴力法。

📝 题目解析:LeetCode 23. 合并 K 个升序链表

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

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

难度:困难(但用分治法其实并不难!)


💡 解题思路:像归并排序一样思考

如果把暴力合并比作“一个一个搬砖”,那分治法就是“成批处理”。

想象一下这个场景
你有 8 堆有序的扑克牌(K=8),你要把它们合成一堆。

  • 暴力法:先把第 1 堆和第 2 堆合在一起(变成一堆大的),再把这堆大的和第 3 堆合……你会发现前面的牌被反复搬运了很多次。

  • 分治法

    1. 先把 8 堆分成 4 组,每组两堆互合(变成 4 堆)。

    2. 再把 4 堆分成 2 组,每组两堆互合(变成 2 堆)。

    3. 最后把 2 堆合为 1 堆。

    • 结果:每张牌被搬运的次数大大减少!

算法流程

  1. 切分:利用递归,将链表数组从中间一分为二,直到每个子数组里只有 1 个链表(或者为空)。
  2. 合并:从最底层开始,两两调用 mergeTwoLists(合并两个有序链表)函数。
  3. 回溯:将合并后的结果逐层向上返回,直到得到最终的大链表。

💻 代码实战:Python 实现

这段代码主要由两部分组成:

  1. merge_sort:负责“分”和“治”的递归逻辑。
  2. merge_two_lists:负责具体的“合”(经典的双指针法)。
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next

class Solution:
    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
        # 1. 边界处理:如果链表数组为空,直接返回 None
        if not lists:
            return None
        
        # 2. 调用分治函数,处理整个数组区间 [0, len(lists)-1]
        return self.merge_sort(lists, 0, len(lists)-1)
    
    # ==========================================
    # 核心递归函数:分治逻辑
    # ==========================================
    def merge_sort(self, lists: List[Optional[ListNode]], left: int, right: int) -> Optional[ListNode]:
        # 递归终止条件:当区间缩小到只剩 1 个链表时,直接返回该链表
        if left == right:
            return lists[left]
        
        # 取中间位置,将大问题拆分为两个小问题
        # (left + right) // 2 是经典的二分法取中点
        mid = (left + right) // 2
        
        # 递归处理左半部分 [left, mid]
        left_head = self.merge_sort(lists, left, mid)
        
        # 递归处理右半部分 [mid+1, right]
        right_head = self.merge_sort(lists, mid+1, right)
        
        # 【关键步骤】合并两个已经排好序的链表
        return self.merge_two_lists(left_head, right_head)
    
    # ==========================================
    # 辅助函数:合并两个有序链表(LeetCode 21题解法)
    # ==========================================
    def merge_two_lists(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
        # 创建虚拟头节点(Dummy Node),避免处理空指针边界情况
        dummy = ListNode(0)
        cur = dummy
        
        # 双指针遍历:谁小接谁
        while l1 and l2:
            if l1.val <= l2.val:
                cur.next = l1   # 接上 l1
                l1 = l1.next    # l1 指针后移
            else:
                cur.next = l2   # 接上 l2
                l2 = l2.next    # l2 指针后移
            cur = cur.next      # 当前指针后移
        
        # 处理剩余节点:
        # 如果 l1 还有剩,直接接在后面;如果 l2 还有剩,接 l2
        cur.next = l1 if l1 else l2
        
        # 返回虚拟头节点的下一个节点,即新链表的头
        return dummy.next

🔍 复杂度分析

时间复杂度: O(Nlog⁡K)

  • K 是链表的个数, N 是所有节点的总数。
  • 分治法会将链表数组切分 log⁡K 层。
  • 每一层合并操作都会遍历所有 N 个节点。
  • 所以总时间是 N×log⁡K 。这比暴力法的 O(N×K) 快得多!

空间复杂度: O(log⁡K)

-   递归调用栈的深度为 log⁡K。

📌 总结

遇到“K 路归并”或者“合并多个有序序列”的问题,优先考虑 分治法堆(优先队列)

  • 分治法:代码简洁,逻辑类似归并排序,适合手写代码面试。
  • :适合数据流场景,或者 K 非常大的情况。

希望这篇笔记能帮你彻底搞定这道“困难”题!如果觉得有用,记得给“爱摸鱼的打工仔”点个赞哦!