告别暴力合并!用“分治法”优雅解决K个链表合并
摘要:本文详解 LeetCode 23 题“合并 K 个升序链表”。通过“分治法”将复杂问题拆解为简单的“两两合并”,结合归并排序思想,将时间复杂度优化至 ,带你掌握处理多路归并的高级技巧。
📚 核心知识点:分治法与多路归并
在处理“合并 K 个有序序列”这类问题时,我们通常有两种思路:
-
暴力法(顺序合并) :先合并前两个,再用结果合并第三个……以此类推。
- 缺点:时间复杂度高,因为参与合并的链表会越来越长。
-
优先队列(最小堆) :维护一个大小为 K 的小顶堆,每次取出最小的节点。
- 优点:效率极高。
-
分治法(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 堆合……你会发现前面的牌被反复搬运了很多次。
-
分治法:
-
先把 8 堆分成 4 组,每组两堆互合(变成 4 堆)。
-
再把 4 堆分成 2 组,每组两堆互合(变成 2 堆)。
-
最后把 2 堆合为 1 堆。
- 结果:每张牌被搬运的次数大大减少!
-
算法流程:
- 切分:利用递归,将链表数组从中间一分为二,直到每个子数组里只有 1 个链表(或者为空)。
- 合并:从最底层开始,两两调用
mergeTwoLists(合并两个有序链表)函数。 - 回溯:将合并后的结果逐层向上返回,直到得到最终的大链表。
💻 代码实战:Python 实现
这段代码主要由两部分组成:
merge_sort:负责“分”和“治”的递归逻辑。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(NlogK)
- K 是链表的个数, N 是所有节点的总数。
- 分治法会将链表数组切分 logK 层。
- 每一层合并操作都会遍历所有 N 个节点。
- 所以总时间是 N×logK 。这比暴力法的 O(N×K) 快得多!
空间复杂度: O(logK)
- 递归调用栈的深度为 logK。
📌 总结
遇到“K 路归并”或者“合并多个有序序列”的问题,优先考虑 分治法 或 堆(优先队列) 。
- 分治法:代码简洁,逻辑类似归并排序,适合手写代码面试。
- 堆:适合数据流场景,或者 K 非常大的情况。
希望这篇笔记能帮你彻底搞定这道“困难”题!如果觉得有用,记得给“爱摸鱼的打工仔”点个赞哦!