LeetCode 23 合并 K 个升序链表从暴力到分治的六种解法与复杂度演进【免费下载链接】leetcodeLeetcode solutions项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode本篇技术指南围绕 LeetCode 23「Merge k Sorted Lists」展开结合当前仓库 merge-k-sorted-linked-lists 提示文档 与 完整题解文章系统梳理合并 k 个有序链表的六种解法——暴力收集排序、逐轮扫描、逐个合并、最小堆、分治递归与分治迭代。读完本文你将掌握每种方案的算法思想、可运行的 Python 实现、时间/空间复杂度推导以及在仓库多语言实现中的对照参考能够在面试与工程实践中根据数据规模做出正确选型。题目概述与复杂度目标题目要求把k个升序链表合并为一个升序链表。为便于统一分析全文约定k表示链表的总个数n表示所有链表中节点的总数。根据 提示文档 的推荐目标解法应达到O(n * k)时间、O(1)空间或更优。也就是说原地复用节点、不申请与n成正比的额外数组是首选方向。提示文档还给出了三条递进式线索暴力做法是把所有n个节点收集进数组、排序、再重建链表代价为O(n log n)——可以思考更优方案例如借助“合并两个有序链表”的思路合并两个有序链表可以做到零额外空间要处理k个链表可以迭代地把每个链表与“已合并的结果链表”两两合并具体实现从下标i 1开始遍历链表数组用mergeTwoLists(lists[i], lists[i - 1])合并将返回的头部存回lists[i]循环结束后最后一个下标处即为完整合并结果。在做题之前建议先掌握以下前置知识仓库中均有对应资料单链表结构、遍历与节点操作合并两个有序链表所有解法的核心原语见 merge-two-sorted-linked-lists 提示 与 对应题解最小堆 / 优先队列用于在 k 个链表中高效找出当前最小节点分治递归地两两拆分再合并排序暴力解法中收集全部值后排序。解法一暴力——收集、排序、重建思路最直白的方式是无视链表结构遍历所有链表把每个节点的值收集进数组排序后按顺序重建一条新链表。实现简单但排序主导了运行时间。算法步骤创建空数组nodes遍历每个链表把每个节点的值追加到nodes对nodes排序使用哑结点dummy head从排序后的数组重建链表返回新链表的头结点。Python 实现# Definition for singly-linked list. # class ListNode: # def __init__(self, val0, nextNone): # self.val val # self.next next class Solution: def mergeKLists(self, lists: List[Optional[ListNode]]) - Optional[ListNode]: nodes [] for lst in lists: while lst: nodes.append(lst.val) lst lst.next nodes.sort() res ListNode(0) cur res for node in nodes: cur.next ListNode(node) cur cur.next return res.next复杂度时间复杂度$O(n \log n)$排序主导空间复杂度$O(n)$存储全部节点值 重建的新链表解法二迭代——每轮扫描最小头结点思路反复在所有链表的当前头结点中挑选最小的那个挂到结果链表的尾部。这等价于“多路归并”的思想每步只看每个非空链表的第一个节点选出值最小者推进该链表指针并把选中节点接入合并结果。算法步骤创建哑结点res指针cur指向它循环直到所有链表为空扫描lists找到当前头结点值最小的链表下标minNode若全部为空则终止把lists[minNode]挂到cur.nextcur前移同时把该链表头指针推进到下一个节点返回res.next。Python 实现class Solution: def mergeKLists(self, lists: List[Optional[ListNode]]) - Optional[ListNode]: res ListNode(0) cur res while True: minNode -1 for i in range(len(lists)): if not lists[i]: continue if minNode -1 or lists[minNode].val lists[i].val: minNode i if minNode -1: break cur.next lists[minNode] lists[minNode] lists[minNode].next cur cur.next return res.next复杂度时间复杂度$O(n * k)$每产出 1 个节点都要扫描 k 条链表空间复杂度$O(1)$原地复用节点无需额外存储解法三逐个合并——提示文档推荐的目标解法这是 提示文档 中 Hint 2、Hint 3 指出的方案也正好满足“O(n*k)时间、O(1)空间”的推荐目标。思路不一次性合并全部链表而是逐一合并先合并lists[0]与lists[1]得到一个有序链表再把结果与lists[2]合并依次类推直到全部合并完毕。每次合并都是标准的“合并两个有序链表”比较两个头结点挂上较小者推进对应指针直到一方为空把另一方剩余部分整体接上。算法步骤若lists为空返回null从下标i 1遍历到k - 1合并lists[i - 1]与lists[i]得到有序链表将结果存回lists[i]循环结束后lists[k - 1]即为完整合并结果返回其头结点。mergeTwoLists(l1, l2) 子过程创建哑结点与tail指针当l1、l2均非空时比较l1.val与l2.val把较小节点接到tail.next推进对应指针与tail若一方仍有剩余节点整体接到tail.next返回dummy.next。Python 实现class Solution: def mergeKLists(self, lists: List[Optional[ListNode]]) - Optional[ListNode]: if len(lists) 0: return None for i in range(1, len(lists)): lists[i] self.mergeList(lists[i - 1], lists[i]) return lists[-1] def mergeList(self, l1, l2): dummy ListNode() tail dummy while l1 and l2: if l1.val l2.val: tail.next l1 l1 l1.next else: tail.next l2 l2 l2.next tail tail.next if l1: tail.next l1 if l2: tail.next l2 return dummy.next复杂度时间复杂度$O(n * k)$第 i 轮合并的链表长度随 i 线性增长各轮合计约为 $O(nk)$空间复杂度$O(1)$全程原地改指针提示Hint 3 中mergeTwoLists(lists[i], lists[i - 1])把结果存回lists[i]本质上就是这里第 2 步的滚动写法——中间结果始终被“带”到数组靠后的位置最终在lists[k-1]收敛。解法四最小堆优先队列思路每轮都扫描全部 k 个头结点太慢改用最小堆把“找全局最小”的成本从O(k)降到O(log k)把每个非空链表的头结点放入堆按节点值排序堆顶始终是当前值最小的节点弹出堆顶接入结果链表若它有后继节点则把后继入堆重复直到堆空。算法步骤创建最小堆将所有非空链表的头结点入堆创建哑结点与指针cur堆非空时弹出最小节点 → 接到cur.next→cur前移 → 若该节点有next把next入堆堆空时所有节点已按序合并返回dummy.next。Python 实现import heapq class NodeWrapper: def __init__(self, node): self.node node def __lt__(self, other): return self.node.val other.node.val class Solution: def mergeKLists(self, lists: List[Optional[ListNode]]) - Optional[ListNode]: if len(lists) 0: return None res ListNode(0) cur res minHeap [] for lst in lists: if lst is not None: heapq.heappush(minHeap, NodeWrapper(lst)) while minHeap: node_wrapper heapq.heappop(minHeap) cur.next node_wrapper.node cur cur.next if node_wrapper.node.next: heapq.heappush(minHeap, NodeWrapper(node_wrapper.node.next)) return res.next说明Python 的heapq默认是小顶堆但由于ListNode不可比较需要像上面一样用NodeWrapper包装并实现__lt__。其他语言的对照实现Java 的PriorityQueue、C 的priority_queue、Go 的heap接口等可查阅 完整题解文章。复杂度时间复杂度$O(n \log k)$空间复杂度$O(k)$堆中最多同时存在 k 个节点解法五分治递归思路借鉴归并排序的思想不按顺序逐个合并而是递归两两合并把 k 条链表从中间分成左右两半递归合并左半 → 一条有序链表递归合并右半 → 一条有序链表最后把两条有序链表合并为一条。每轮两两合并的代价与参与链表总长度成正比总共约log k层因此比顺序逐个合并更高效。算法步骤基准情形lists为空返回null递归函数divide(lists, l, r)l r返回nulll r返回lists[l]分治步骤mid (l r) // 2递归计算left divide(lists, l, mid)、right divide(lists, mid 1, r)合并步骤用标准双链表合并例程把left、right合并返回结果最终答案调用divide(lists, 0, len(lists) - 1)。Python 实现class Solution: def mergeKLists(self, lists): if not lists or len(lists) 0: return None return self.divide(lists, 0, len(lists) - 1) def divide(self, lists, l, r): if l r: return None if l r: return lists[l] mid l (r - l) // 2 left self.divide(lists, l, mid) right self.divide(lists, mid 1, r) return self.conquer(left, right) def conquer(self, l1, l2): dummy ListNode(0) curr dummy while l1 and l2: if l1.val l2.val: curr.next l1 l1 l1.next else: curr.next l2 l2 l2.next curr curr.next if l1: curr.next l1 else: curr.next l2 return dummy.next复杂度时间复杂度$O(n \log k)$空间复杂度$O(\log k)$递归栈深度解法六分治迭代思路与解法五同属分治思想但用循环代替递归每轮把链表按步长 2 两两配对合并得到约一半数量的链表重复直到只剩一条。一轮内合并list0与list1→M0合并list2与list3→M1……每轮过后链表数量减半循环直到只剩 1 条链表。算法步骤lists为空返回null当链表数量大于 1创建空数组mergedLists以步长 2 遍历lists取l1 lists[i]l2 lists[i 1]不存在则为None合并后追加到mergedListslists mergedLists循环结束时lists[0]即完整合并结果。mergeList(l1, l2)与解法三中的子过程完全一致哑结点 tail 指针 接剩余。Python 实现class Solution: def mergeKLists(self, lists: List[Optional[ListNode]]) - Optional[ListNode]: if not lists or len(lists) 0: return None while len(lists) 1: mergedLists [] for i in range(0, len(lists), 2): l1 lists[i] l2 lists[i 1] if (i 1) len(lists) else None mergedLists.append(self.mergeList(l1, l2)) lists mergedLists return lists[0] def mergeList(self, l1, l2): dummy ListNode() tail dummy while l1 and l2: if l1.val l2.val: tail.next l1 l1 l1.next else: tail.next l2 l2 l2.next tail tail.next if l1: tail.next l1 if l2: tail.next l2 return dummy.next仓库实测当前仓库的 Python 实现 采用的正是这一版“迭代分治”写法可作为可运行、可验证的参考实现。复杂度时间复杂度$O(n \log k)$空间复杂度$O(k)$每轮mergedLists暂存中间结果常见陷阱未处理输入数组中的空链表lists中可能包含null或空链表访问节点值前必须判空否则会触发空指针异常。忘记推进被选中链表的指针选中 k 条链表中最小节点后必须把该链表头指针移到下一个节点漏掉这一步会导致同一节点被反复选中形成死循环。最小堆比较器写反部分语言默认堆是“大顶堆”如 C 的priority_queue需要自定义比较器按节点值升序方向写反会得到一个大顶堆输出顺序错误。Python 的heapq虽是小顶堆但需通过包装类实现节点比较见解法四。结果链表不用哑结点不使用哑结点时第一个节点的插入需要特判逻辑更繁琐统一用哑结点可以让所有节点处理方式一致最后返回dummy.next。迭代过程中修改输入数组逐个合并或分治时原地覆写lists数组元素可能导致漏合并或错误合并。要么用独立数组存放合并结果要么严格按索引谨慎推进解法三中“结果存回lists[i]、下次用lists[i-1]”的顺序即是一种安全写法。六种解法复杂度对照解法时间复杂度空间复杂度特点暴力收集 排序$O(n \log n)$$O(n)$实现最简单但空间开销大迭代每轮扫描最小头$O(n * k)$$O(1)$零额外空间适合 k 较小时逐个合并$O(n * k)$$O(1)$提示文档推荐的目标方案零额外空间最小堆$O(n \log k)$$O(k)$k 很大时最优需注意比较器方向分治递归$O(n \log k)$$O(\log k)$归并排序思想递归栈较浅分治迭代$O(n \log k)$$O(k)$仓库 Python 实测实现无递归栈风险其中k为链表总数n为所有链表的节点总数。面试中若要求O(1)额外空间优先选解法二或解法三若不限制空间且 k 较大解法四堆与解法五/六分治的 $O(n \log k)$ 更优。仓库多语言实现参考解题思路与全部 9 种语言的完整代码Python / Java / C / JavaScript / C# / Go / Kotlin / Swift / Rustarticles/merge-k-sorted-linked-lists.md逐步提示复杂度目标与三条递进线索hints/merge-k-sorted-linked-lists.md仓库可运行实现迭代分治Pythonpython/0023-merge-k-sorted-lists.py前置知识——合并两个有序链表提示文档 与 完整题解实际练习时建议先在纸上推演“逐个合并”的指针变化过程再用最小堆优化最后用分治对比复杂度差异即可牢固掌握这一类“多路归并”问题。【免费下载链接】leetcodeLeetcode solutions项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考