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

LeetCode 148. 排序链表:归并排序详解

拆解 LeetCode 中等难度题目「148. 排序链表」,这道题核心考察链表的归并排序,是链表操作与排序算法结合的经典题型,也是面试中高频出现的考点。本文会从题目分析、解题思路、代码拆解到注意事项,一步步帮大家搞懂这道题,新手也能轻松跟上。

一、题目回顾

题目要求:给你链表的头结点 head ,请将其按升序排列并返回排序后的链表。

补充说明:

  • 链表中节点的数目范围是 [0, 5 * 10⁴]

  • -10⁵ ≤ Node.val ≤ 10⁵

进阶要求:你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?(本文解法将满足这一要求)

二、解题思路:为什么选择归并排序?

首先思考:链表排序和数组排序有什么区别?数组可以用快速排序(随机访问效率高),但链表的随机访问效率低(O(n)),而归并排序的核心是「分治+合并」,刚好适配链表的特性——分治时无需随机访问,合并时只需调整指针,且时间复杂度天然是 O(n log n),完美契合题目要求。

归并排序的整体思路(链表适配版):

  1. 分:将链表从中间分成左右两个子链表,递归拆分,直到每个子链表只包含一个节点(单个节点本身就是有序的);

  2. 治:将两个有序的子链表合并成一个有序的链表;

  3. 合:递归合并所有子链表,最终得到完整的有序链表。

关键难点:如何找到链表的中间节点(分治的核心)、如何高效合并两个有序链表。

三、完整代码(TypeScript 版)

先贴出完整可运行代码,后续逐部分拆解,大家可以先整体感知:

classListNode{val:numbernext:ListNode|nullconstructor(val?:number,next?:ListNode|null){this.val=(val===undefined?0:val)this.next=(next===undefined?null:next)}}functionsortList(head:ListNode|null):ListNode|null{// 边界处理:空链表直接返回if(head===null){returnnull;}// 1. 找到链表中间节点(二分核心)constfindMid=(left:ListNode,right:ListNode|null):ListNode=>{if(left===right){returnleft;}// 快慢指针:慢指针走1步,快指针走2步,快指针到尾时,慢指针在中间letslow:ListNode|null|undefined=left;letfast:ListNode|null|undefined=left.next;// 快指针不越界(right是区间边界,不包含)while(fast!==null&&fast?.next!==null){slow=slow.next!;fast=fast.next?.next;}returnslow;};// 2. 递归分治+合并constdfs=(left:ListNode|null,right:ListNode|null):ListNode|null=>{if(left===null){returnnull;}// 单个节点,直接返回(递归终止条件)if(left===right){returnleft;}// 找中间节点,拆分链表constmid=findMid(left,right);constrightHead=mid.next;// 右子链表的头节点mid.next=null;// 切断左右子链表,避免循环// 递归拆分左、右子链表lettemp_1=dfs(left,mid);lettemp_2=dfs(rightHead,right);// 合并两个有序子链表returnmergeTwoLists(temp_1,temp_2);};// 3. 合并两个有序链表(辅助函数)constmergeTwoLists=(l1:ListNode|null,l2:ListNode|null):ListNode|null=>{constdummyHead=newListNode(0);// 虚拟头节点,简化合并逻辑lettemp=dummyHead;// 双指针遍历两个链表,按值大小拼接while(l1!==null&&l2!==null){if(l1.val<=l2.val){temp.next=l1;l1=l1.next;}else{temp.next=l2;l2=l2.next;}temp=temp.next;// 移动指针}// 拼接剩余节点(其中一个链表可能已遍历完)temp.next=l1!==null?l1:l2;returndummyHead.next;// 返回合并后的链表头};// 调用递归函数,初始区间:头节点到null(右边界不包含)returndfs(head,null);};

四、代码逐部分拆解

1. 链表节点类(ListNode)

题目已给出节点类的定义,核心是两个属性:

  • val:节点存储的值(number类型);

  • next:指向后续节点的指针(可能为null,代表链表尾)。

构造函数做了默认值处理:val默认0,next默认null,避免传入undefined时报错。

2. 边界处理

if(head===null){returnnull;}

当输入链表为空时,直接返回null,避免后续递归出现异常,这是所有链表题的常规操作。

3. 找到链表中间节点(findMid函数)

这是分治的核心,采用「快慢指针法」,效率比遍历计数找中间节点更高(O(n/2) 时间)。

核心逻辑:

  • 慢指针(slow)初始指向left,每次走1步;

  • 快指针(fast)初始指向left.next,每次走2步;

  • 当快指针无法再走2步(fast === null 或 fast.next === null)时,慢指针刚好指向链表的中间节点。

注意:函数参数中的right是「区间边界,不包含」,比如初始调用时right为null,代表拆分到链表末尾。

4. 递归分治(dfs函数)

dfs函数的作用是「拆分链表+合并链表」,递归终止条件是:当left === right时,说明当前子链表只有一个节点,直接返回(单个节点有序)。

关键步骤:

  1. 调用findMid找到中间节点mid;

  2. 将mid.next赋值给rightHead(右子链表的头),并将mid.next设为null,切断左右子链表(避免递归时循环遍历);

  3. 递归调用dfs,分别处理左子链表(left到mid)和右子链表(rightHead到right);

  4. 调用mergeTwoLists,将两个有序的子链表合并,返回合并后的链表头。

5. 合并两个有序链表(mergeTwoLists函数)

这是归并排序的「治」环节,也是链表操作的经典场景,采用「虚拟头节点+双指针」实现。

核心逻辑:

  • 创建虚拟头节点dummyHead(值为0,无实际意义),用于简化链表拼接逻辑(无需单独处理头节点);

  • temp指针指向dummyHead,用于遍历拼接新链表;

  • 双指针l1、l2分别遍历两个有序子链表,每次将值较小的节点拼接到temp.next,然后移动对应指针;

  • 当其中一个链表遍历完后,将另一个链表的剩余节点直接拼接到temp.next(剩余节点已有序);

  • 返回dummyHead.next,即合并后的有序链表头。

6. 主函数调用

returndfs(head,null);

初始调用dfs时,左边界为head(链表头),右边界为null(不包含,即链表尾),开启递归分治与合并。

五、关键注意事项(避坑点)

  1. 拆分链表时,必须将mid.next设为null,否则左右子链表会相连,递归时会陷入死循环;

  2. 快慢指针的初始位置:fast要比slow超前一步(left.next),否则当链表长度为2时,会无法拆分(比如1→2,slow和fast都指向1,无法分成[1]和[2]);

  3. 合并链表时,虚拟头节点的使用是关键,能避免单独判断头节点的麻烦,提升代码简洁度;

  4. 递归终止条件:left === right(单个节点),否则会无限递归,导致栈溢出。

六、复杂度分析(满足进阶要求)

  • 时间复杂度:O(n log n),归并排序的标准时间复杂度,分治环节是O(log n)层,每一层的合并环节是O(n),整体为O(n log n);

  • 空间复杂度:O(log n),递归调用栈的深度为O(log n)(分治的层数),没有使用额外的数组或哈希表,满足常数级空间的进阶要求(若不考虑递归栈,空间复杂度为O(1))。

七、总结

LeetCode 148.排序链表的核心是「链表的归并排序」,核心难点在于「找中间节点」和「合并有序链表」,而快慢指针和虚拟头节点是解决这两个难点的关键技巧。

这道题的价值在于,它不仅考察了链表的基本操作,还融合了归并排序的分治思想,是面试中考察「链表+排序」的典型代表。建议大家多动手调试代码,重点理解拆分链表时的指针切断、合并链表时的双指针移动,掌握后能轻松应对同类链表排序问题。

http://www.jsqmd.com/news/486320/

相关文章:

  • 深度学习学习笔记
  • 探索格子玻尔兹曼(LBM)下多孔介质水气分布规律(D3q19模型)
  • 攀枝花商家如何实现24小时无人直播?AI智能系统解锁流量新密
  • COMSOL案例:多孔介质中渗漏模拟的实践
  • 伪随机码PRBS与线性反馈移位寄存器LFSR
  • 纯水设备专业厂家
  • 3:《死亡笔记》功利主义+报应正义:基拉如何让重罪率暴跌并拯救潜在受害者
  • 智能净水器九大安全防护技术解析
  • Mac电脑配置环境变量
  • 欧姆龙CP1H与台达VFD - M变频器的MODBUS RTU通讯实战
  • 在 Kata Containers 中编译支持 eBPF 的 Guest Kernel 并验证生效
  • MySQL【基本查询下 - 表的增删改查】
  • 为2026年营销活动找富士山素材,这五类站点的筛选顺序很重要
  • 信号与系统分析2026(春季)作业要求:第五次作业
  • Agent Hub:给你的 OpenClaw 装一个多模型军团
  • 基于C语言的轻量级在线商城服务端设计与实现
  • sdut-程序设计基础Ⅰ-实验7-函数(函数题)
  • 淘宝商品详情字段解析:SKU、价格、库存接口全梳理
  • HakcMyVM-Darkside
  • Java Map 集合深度解析(HashMap / ConcurrentHashMap 原理详解)
  • 创建了项目实训博客
  • 基于VirtualLab Fusion的复合光源仿真
  • 计算机毕业设计springboot基于Spark的豆瓣电影数据分析与可视化系统 基于SpringBoot与Spark的豆瓣影片数据挖掘及可视化平台 SpringBoot框架下融合Spark的豆瓣影视信
  • 一篇看懂:进程、服务、启动项、计划任务到底是什么?
  • hot 100 300.最长递增子序列
  • 六城高端腕表维修实操指南:36品牌故障应急+正规网点避坑(表主实测版) - 时光修表匠
  • 第三章:机器学习初醒:从数据中寻找规律
  • 算法设计与分析-习题4.3
  • 2026年青浦区高质量家电门店TOP榜:哪几家值得优先光顾?
  • 零基础Java Web初学者(三):Servlet的两种配置方法