当前位置: 首页 > news >正文

【笔面试算法学习专栏】合并K个升序链表:堆与分治的完美结合

学习目标

  • 理解最小堆(优先队列)在算法中的应用
  • 掌握分治思想解决多路合并问题
  • 能够分析K路归并的复杂度
  • 理解为什么堆比直接两两合并更优
  • 为处理海量数据排序问题打下基础

一、问题引入

1.1 题目描述

LeetCode 23:给定k个升序链表数组,将所有链表合并到一个升序链表中。

示例

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

约束

  • k = lists.length,范围 [0, 10⁴]
  • 每个链表节点数范围 [0, 200]
  • 期望时间复杂度:O(N log k),N为总节点数

1.2 为什么不能直接遍历?

最朴素的做法:把所有节点值放到数组,排序后再建链表。

defmergeKLists_naive(lists):vals=[]forlstinlists:whilelst:vals.append(lst.val)lst=lst.nextvals.sort()# 重建链表...

问题:时间复杂度O(N log N),但题目要求O(N log k)。对于N很大但k相对小的情况,log k < log N。

二、解法一:最小堆(Priority Queue)

2.1 堆的核心思想

堆是一棵完全二叉树,小顶堆保证父节点值 ≤ 子节点值。插入/删除时间复杂度都是O(log k)。

为什么用堆?想象K路赛跑,每条链表是最慢的跑步者队列。堆帮你快速找到"下一位最快要出场的选手"。

2.2 算法步骤

1. 初始化:把每个链表的头节点放入最小堆 2. 循环: a. 弹出堆顶(当前最小节点) b. 将弹出的节点接到结果链表 c. 若该节点有后继,把后继入堆 3. 堆为空时,合并完成

2.3 代码实现

importheapqdefmergeKLists(lists):# 边界处理heap=[]fori,nodeinenumerate(lists):ifnode:# (节点值, 链表索引, 节点) - 用索引解决值相同时的比较问题heapq.heappush(heap,(node.val,i,node))dummy=ListNode(0)curr=dummywhileheap:val,i,node=heapq.heappop(heap)curr.next=node curr=curr.nextifnode.next:heapq.heappush(heap,(node.next.val,i,node.next))returndummy.next

为什么要用索引i作为第二比较元?因为两个节点val相同时,heapq会尝试比较节点对象,导致类型错误。

2.4 复杂度分析

指标分析
时间复杂度O(N log k),每个节点入堆出堆各一次
空间复杂度O(k),堆最多存k个元素

三、解法二:分治归并

3.1 分治思想

分治算法的经典模式:分解 → 解决 → 合并

对于K个链表,我们可以用归并排序的思路:

  • 每次把问题规模减半
  • 直到只剩一个或零个链表
  • 两两合并,层层回溯

图示:分治合并过程

[1→4→5] [1→3→4] [2→6] ↓分 ↓分 ↓分 [1→4→5] [1→3→4] [2→6] ↓合并 ↓空 [1→1→3→4→4→5] [2→6] ↓合并 [1→1→2→3→4→4→5→6]

3.2 代码实现

defmergeKLists(lists):ifnotlists:returnNonereturnmerge_sort(lists,0,len(lists)-1)defmerge_sort(lists,left,right):ifleft==right:returnlists[left]mid=(left+right)//2left_list=merge_sort(lists,left,mid)right_list=merge_sort(lists,mid+1,right)returnmerge_two(left_list,right_list)defmerge_two(l1,l2):dummy=ListNode(0)curr=dummywhilel1andl2:ifl1.val<=l2.val:curr.next=l1 l1=l1.nextelse:curr.next=l2 l2=l2.nextcurr=curr.nextcurr.next=l1orl2returndummy.next

3.3 复杂度分析

指标分析
时间复杂度O(N log k),树高为log k,每层合并N个节点
空间复杂度O(log k),递归栈深度

四、两种解法对比

特性最小堆分治归并
时间复杂度O(N log k)O(N log k)
空间复杂度O(k)O(log k)
代码复杂度中等较简单
适用场景数据流式输入已知全部数据
优点内存稳定递归简洁

面试建议:两种都能讲明白最佳。如果时间紧,分治更容易实现无误。

五、扩展:多路归并在工程中的应用

5.1 海量日志排序

假设有1000个文件,每个文件内部有序,但整体无序。如何高效合并?

思路:每次从1000个文件各读一行,放到最小堆,取出最小的写入输出文件,再从该文件补充一行。

5.2 外部排序

当数据量远超内存时,最小堆是外部排序的核心算法。

# 简化的外部排序思路defexternal_sort(input_files,output_file):# 1. 分批次读入内存,排序,写入临时文件# 2. 每次从各临时文件读取一块到内存# 3. 用最小堆合并,输出到最终文件pass

六、面试中的延伸问题

6.1 如果K=1怎么办?

直接返回该链表。代码中if not lists已处理。

6.2 如果某链表为空怎么办?

在初始化堆时跳过空链表:if node: heapq.heappush(...)

6.3 如何做到O(N)时间复杂度?

不可能。因为我们需要对N个元素进行排序,最优是O(N log N)。K路归并的O(N log k)已经是在"已知各链表有序"这个先验信息下的最优解。

6.4 进阶:如何支持自定义比较规则?

classNode:def__init__(self,val,index,node):self.val=val self.index=index self.node=nodedef__lt__(self,other):returnself.val<other.val# 自定义比较逻辑

七、代码模板总结

# 最小堆模板importheapqdefmerge_with_heap(lists):heap=[(node.val,i,node)fori,nodeinenumerate(lists)ifnode]heapq.heapify(heap)dummy=curr=ListNode(0)whileheap:val,i,node=heapq.heappop(heap)curr.next=node curr=curr.nextifnode.next:heapq.heappush(heap,(node.next.val,i,node.next))returndummy.next

八、总结

合并K个有序链表展示了两种经典算法思想:

  1. :始终维护"当前最小",适合数据流式处理
  2. 分治:化繁为简,将K路问题分解为log k层两路合并

面试加分项

  • 解释为什么不用朴素O(N log N)排序
  • 分析空间复杂度差异
  • 举一反三到外部排序、海量数据处理

习题

  1. LeetCode 21:合并两个有序链表(基础,必须熟练)
  2. LeetCode 147:对链表进行插入排序
  3. LeetCode 148:链表的归并排序(原地排序)
  4. 思考:如何用最小堆求第K小的数?

参考文献

  • LeetCode 23. Merge k Sorted Lists
  • 《算法导论》第六章:堆排序
  • 《剑指Offer》链表章节
http://www.jsqmd.com/news/650464/

相关文章:

  • 单元测试的隐秘角落:如何优雅地“窥探”private方法?
  • Spring-Boot-枚举使用-这8个坑90的人都踩过
  • 2026年开源客服系统哪家好?大模型多语言数据分析呼叫中心集成 - 品牌2026
  • 别再只会点菜单了!EPLAN拖放操作全解析:从符号宏到DWG文件,效率翻倍的隐藏技巧
  • 分析想找小班授课的形象设计培训学校,太原哪家比较靠谱 - 工业品网
  • 从静态防护到流转治理:API风险监测系统如何重塑企业数据安全体系
  • 抖音无水印批量下载工具:如何轻松保存你喜欢的视频内容?
  • Unity WebGL 缓存失效排查:从 Cache API 错误到 loader.js 修复
  • 小目标检测技术演进:从数据增强到无锚点方法的全面解析
  • Matlab图像显示进阶:pcolor与imagesc的格网精细化控制
  • 2026年在线客服哪家好?客服系统机器人推荐及选型指南 - 品牌2026
  • 保姆级教程:用群晖Docker和technosoft2000镜像,5分钟搞定Calibre Web私人书库(附权限避坑指南)
  • 终极中文文献管理方案:如何用Jasminum插件解决Zotero中文元数据识别难题
  • 基于STM32的TCRT5000循迹传感器实战指南:从原理到代码实现
  • 【从0开始学设计模式-8| 桥接模式】
  • 给测试新人的TBOX入门指南:从零看懂车载通信测试到底在测啥
  • 阿里放大招!Qwen3.5-Omni发布,企业AI落地成本大幅降低
  • 2026年新疆乌鲁木齐:车闪电新能源汽车防护升级服务全景报道 - 精选优质企业推荐榜
  • 如何快速实现B站m4s视频格式转换:3分钟无损转换完整指南
  • vxe-table 自定义单元格提示模板实战:从基础配置到高级应用
  • CAN离线记录仪从入门到精通:手把手教你配置与使用(附常见问题解决)
  • 魔兽世界GSE宏编辑器终极指南:5步打造你的智能技能循环
  • 终极番茄小说下载器:从网页到电子书的完整解决方案
  • 【MySQL】深入解析 Handler 接口:从语法到实战的逐行数据操作指南
  • 2026年呼和浩特GEO优化领域3家主流服务商选型参考深度分析报告 - 商业小白条
  • 生成式AI灰度发布失败率下降73%的关键策略:从流量切分、语义一致性校验到回滚SLA量化设计
  • 从游戏私服后台到系统权限:一次ASPcms漏洞的完整利用链剖析
  • 杰理之PC硬回踩没效果【篇】
  • 轻量翻译模型HY-MT1.5-1.8B:术语干预功能使用教程
  • 牛客网热门Java 面试八股文解析 + 大厂面试攻略