news 2026/9/1 5:11:33

最稳的分治排序!归并排序凭什么稳定 O(n log n),还顺手帮你数清逆序对?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
最稳的分治排序!归并排序凭什么稳定 O(n log n),还顺手帮你数清逆序对?

昨天我们被快排的随机化和三路切分折腾得够呛,今天换个“老实人”——归并排序
它没有快排的花哨,但胜在稳定:最好、最坏、平均都是O(nlogn),且天生稳定,不需要任何随机化技巧。

更妙的是,在归并的过程中,你可以顺手数出数组中的逆序对数量——这本来是Hard级别的题(剑指 Offer51),但归并排序只需加一行代码就能搞定,复杂度依然是 O(nlogn)。

今天我们就用归并排序搞定LC.912,再顺势拿下逆序对计数,一箭双雕。


📦 题目速览(30 秒读懂)

题目1:排序数组(LC.912)

给你一个整数数组nums,将其升序排列。
示例:[5,2,3,1][1,2,3,5]

题目2:逆序对计数(剑指 Offer51 / LCR170)

如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。求数组中逆序对的总数。
示例:[7,5,6,4]→ 输出5(解释:(7,5),(7,6),(7,4),(5,4),(6,4))
约束:长度 ≤ 5×10⁴,暴力O(n²)必挂。


🧠 核心思路:分治的另一种姿势——先拆后合,逆序对藏在合并里

暴力慢在哪?

  • 排序:选择/插入O(n²) → 超时。
  • 逆序对:双重循环枚举所有i<j,比较nums[i] > nums[j]→ O(n²),且逆序对数量本身可达O(n²)(倒序数组),但我们需要的是计数而不是枚举。

归并排序的“分治”哲学

  • :把数组从中间一分为二,递归排序左右两半,直到每半只剩一个元素(天然有序)。
  • :把两个有序数组合并成一个更大的有序数组——用双指针分别扫描,每次取较小者放入临时数组。

为什么快?合并两个总长为n的有序数组只需O(n),递归树高度O(logn),总O(nlogn)。

逆序对怎么“免费”数出来?

当合并左右两半时,左右内部都已经有序。此时如果右半的当前元素right[j]小于左半的当前元素left[i],那么由于左半有序,left[i]到左半末尾的所有元素都大于right[j],并且它们原始位置都在right[j]之前。所以可以一口气数出mid - i + 1个逆序对(i是当前左指针的位置)。

这就是“批发”计数,而不是一个一个枚举,从而把O(n²) 优化到O(nlogn)。

关键:归并排序本身就是有序合并的过程,逆序对计数只是多累加一行,完全免费。


🖼️ 图解算法(逆序对计数过程)

nums = [7,5,6,4]为例,递归拆分:

[7,5,6,4] ├─ [7,5] ──┬─ [7] │ └─ [5] └─ [6,4] ──┬─ [6] └─ [4]

自底向上合并,关注逆序对产生:

合并左半(有序)右半(有序)触发逆序对的情况计数累加
[7] 与 [5][7][5]5<7,左半剩余 [7] 都与5成对+1
[6] 与 [4][6][4]4<6,左半剩余 [6] 都与4成对+1
[5,7] 与 [4,6][5,7][4,6]4<5,左半 [5,7] 都与4成对 → +2;6>5取5不计;6<7,左半 [7] 与6成对 → +1+3

总计数 = 1+1+3 =5,正确 ✅

可见,每次“右半元素被取走而左半还有剩余”时,批量计入逆序对。


💻 代码实现(Python + Java,二合一)

Python 版(归并排序 + 逆序对可选)

classSolution:defsortArray(self,nums:List[int])->List[int]:self.merge_sort(nums,0,len(nums)-1)returnnumsdefmerge_sort(self,nums,lo,hi):iflo>=hi:returnmid=(lo+hi)//2self.merge_sort(nums,lo,mid)self.merge_sort(nums,mid+1,hi)self.merge(nums,lo,mid,hi)defmerge(self,nums,lo,mid,hi):tmp=nums[lo:hi+1]# 复制到临时数组i,j=0,mid-lo+1# i左半起点,j右半起点(相对于tmp)forkinrange(lo,hi+1):ifi>mid-lo:# 左半用完nums[k]=tmp[j];j+=1elifj>hi-lo:# 右半用完nums[k]=tmp[i];i+=1eliftmp[i]<=tmp[j]:# 相等取左 → 保持稳定nums[k]=tmp[i];i+=1else:# 若需数逆序对,在这里加:# count += (mid - lo) - i + 1nums[k]=tmp[j];j+=1

Java 版(专门用于逆序对计数,LCR 170)

classSolution{privateintcount=0;publicintreversePairs(int[]record){if(record.length<2)return0;mergeSort(record,0,record.length-1);returncount;}privatevoidmergeSort(int[]nums,intlo,inthi){if(lo>=hi)return;intmid=lo+(hi-lo)/2;mergeSort(nums,lo,mid);mergeSort(nums,mid+1,hi);merge(nums,lo,mid,hi);}privatevoidmerge(int[]nums,intlo,intmid,inthi){int[]tmp=newint[hi-lo+1];System.arraycopy(nums,lo,tmp,0,hi-lo+1);inti=0,j=mid-lo+1;for(intk=lo;k<=hi;k++){if(i>mid-lo){nums[k]=tmp[j++];}elseif(j>hi-lo){nums[k]=tmp[i++];}elseif(tmp[i]<=tmp[j]){// 相等取左,稳定nums[k]=tmp[i++];}else{// 关键:右半较小,左半剩余全部 > tmp[j]count+=(mid-lo)-i+1;nums[k]=tmp[j++];}}}}

⚠️关键点

  • 稳定性来自tmp[i] <= tmp[j]时取左半,相等时左半原位置在前,顺序保留。
  • 逆序对计数只需在else分支加一行count += (mid - lo) - i + 1,其余不变。
  • 临时数组tmp是必需的,因为合并时原数组会被覆盖,无法同时读取左右段。

⏱️ 复杂度分析(面试必问)

  • 时间:每层合并O(n),共logn 层 →O(nlogn),不依赖数据分布,稳定可靠。
  • 空间:临时数组O(n) + 递归栈 O(logn) →O(n)。这是稳定性和确定性性能的代价。

🚀 举一反三:4 道高频变种题,一套框架通吃

题目变化点应对策略
LC.493 翻转对统计nums[i] > 2*nums[j]的数量归并框架,但在 merge之前用双指针单独统计(因为 2 倍关系与归并顺序不完全同步)
LC.327 区间和的个数统计满足条件的子数组和个数前缀和 + 归并计数,同一套路
LC.148 排序链表对链表排序归并排序是链表排序的首选(快排在链表上partition别扭)
LC.剑指 Offer 51纯逆序对计数直接套上面的Java版即可

💬 面试追问模拟(提前准备,惊艳全场)

Q1:归并排序为什么稳定?

因为合并时,当左右元素相等,我们优先取左半(使用<=判断)。这样左半中相等的元素会先被放入结果,它们原始顺序保持不变,整体稳定性得以维持。

Q2:归并排序和快排,你选哪个?

  • 归并:稳定、最坏 O(n log n)、需要 O(n) 额外空间 → 适合对象排序、外部排序、需要稳定性的场景。
  • 快排:不稳定、最坏 O(n²)(随机化后概率低)、原地排序、缓存友好 → 适合基本类型排序、内存敏感场景。
    Java 的Arrays.sort()对基本类型用快排变体,对对象用 TimSort(归并 + 插入)。

Q3:外部排序是怎么用归并思想的?

数据太大装不进内存:分批读入内存,排好序写成有序的“归并段”(run),然后对这些段进行多路归并(用小顶堆或败者树)合并成一个大文件。多路归并可以减少磁盘 I/O 次数,是数据库和 MapReduce 的基础。

Q4:逆序对计数能不能用快排?

不能。快排不涉及两个有序子数组的合并过程,无法批量获得跨区间的逆序信息。归并排序天然适合这种“跨左右统计”的问题。


🧩 实战小技巧(刷题党必备)

  • 口诀:拆到底,合有序;相等取左稳;跨区间,批量数。
  • 模板:凡是需要“统计跨左右区间的某种关系”且区间内有序可复用,优先考虑归并排序。
  • 防坑:临时数组一定要复制完整区间;索引计算别搞错(特别是 mid 的偏移)。

📈 实际应用场景(不止是刷题)

  • Java 对象排序Arrays.sort(Object[])使用 TimSort(归并 + 插入排序优化)。
  • 数据库外部排序:处理大文件排序时的归并阶段。
  • 稳定多关键字排序:先按部门排,再按工资排,稳定保持部门内顺序。
  • Git 合并:合并有序提交历史树时也用归并思想。

🎁 今日思考题

如果题目要求统计“非严格逆序对”(即nums[i] >= nums[j]就算),代码需要改哪里?
提示:只需将tmp[i] <= tmp[j]改成<,这样相等时会取右半,左半剩余与右半相等元素都计入。
你能写出这个改动吗?

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/1 5:10:24

机器人运动控制实战:从硬件选型到PID算法实现

这类课程最值得关注的不是理论有多深&#xff0c;而是能不能把“机器人动起来”这个核心目标拆解成可执行的步骤。ROB311这类课程&#xff0c;或者任何想从零开始做机器人的项目&#xff0c;关键不在于你掌握了多少公式&#xff0c;而在于你能不能把机械、电子、控制、软件这几…

作者头像 李华
网站建设 2026/9/1 5:09:51

计算机视觉与 自然语言处理 算法落地实践:交付前的最后检查怎么做

计算机视觉与 自然语言处理 算法落地实践&#xff1a;交付前的最后检查怎么做 讨论时&#xff0c;发布前的预检阶段&#xff0c;团队用畸形输入验证系统的边界行为。 算法团队准备交付最新研发的跨模态商品识别与标题自动生成服务。在离线测试集中&#xff0c;模型的 mAP 达到…

作者头像 李华
网站建设 2026/9/1 5:07:53

gdal244_mingw64.rar 使用指南:MinGW 环境下 GDAL 配置与 Qt 集成

简介&#xff1a;gdal244_mingw64.rar 是一份面向 Qt5 的 GDAL 2.4.4 MinGW64 预编译资源包&#xff0c;适合在 Windows 64 位环境中进行地理信息系统或遥感应用开发的中高级工程师。它解决了 Qt 工程里集成 GDAL 时常见的编译与配置问题&#xff0c;开发者无需从源码构建&…

作者头像 李华
网站建设 2026/9/1 5:07:47

华为AI岗备考全流程:从OD机试到面试定级实战指南

去年我把求职目标定在华为AI岗的时候&#xff0c;身边不少朋友觉得我是在赌一个“大厂光环”。但真正走完投递、机试、技术面、主管面整个流程之后&#xff0c;我想说的是&#xff1a;这个岗位的竞争激烈程度&#xff0c;以及需要准备的深度&#xff0c;远不是刷几道LeetCode就…

作者头像 李华
网站建设 2026/9/1 5:07:31

2026华为AI岗通关复盘:从OD机试到大模型Agent面试全攻略

7月15号那天&#xff0c;我拿到华为工卡刷开门禁&#xff0c;没急着去工位&#xff0c;先在大厅坐了一会儿。年初开始投简历、刷机试、一轮轮面试&#xff0c;到真正站在园区里&#xff0c;这个“2026年华为AI岗”的标签才算落了地。期间踩过的坑、补过的课、临时抱佛脚的夜&am…

作者头像 李华
网站建设 2026/9/1 5:06:55

美赛各题型参考代码整理:从模板到实战的高效复用指南

简介&#xff1a;一份面向数学建模竞赛&#xff08;国赛、美赛&#xff09;参赛者的常见题型参考代码汇总&#xff0c;覆盖从经典线性回归、聚类分析、主成分分析到遗传算法改进神经网络等智能算法模型&#xff0c;对正在备赛冲刺或需要快速搭建基线模型的选手很有帮助。压缩包…

作者头像 李华