每个刷力扣的人应该都经历过这种卡壳:明明题目看懂了,暴力法也写出来了,但一提交就是超时,看题解又觉得别人思路太绕。尤其是分治和归并排序这类题目,代码量不大,但递归的展开过程、区间的划分、合并时的细节,每一步都可能让你怀疑自己适不适合写代码。这篇内容我想用两道经典的力扣题——排序数组和交易逆序对的总数——把归并排序这条线彻底讲透,从原理到代码,从易错点到优化思路,一次说清楚。
先交代一下这篇文章适合谁看:如果你正在刷力扣热题100,或者准备面试需要系统过一遍分治/排序类题目,又或者你其实已经能写出归并排序但不太清楚它到底能解决什么进阶问题,那么这篇内容值得你花二十分钟认真读一遍。我会尽量用现场写代码时的真实思路来讲,而不是给你一份冷冰冰的参考答案。
1. 整体设计与思路拆解:两道题为什么放在一起讲
1.1 分治与归并排序的本质
先理解归并排序的本质。它做的事情只有三步:把数组从中间切成两半,分别把左半边和右半边排好序,然后把两个有序的子数组合并成一个整体有序的数组。这三步对应到代码上就是两个函数:一个负责递归切分,一个负责合并。
很多初学者会把注意力放在“切分”上,觉得递归入口和退出条件是难点。但实际上,切分只是把问题规模不断缩小,真正决定归并排序性能和正确性的,是合并那一步。合并的过程也很简单:维护两个指针分别指向左子数组和右子数组的头部,每次取较小的那个放入结果数组,然后移动对应的指针。听起来简单,但当你真正动手写的时候会发现,边界条件、指针移动、临时数组的分配,任何一个地方出错,跑出来的结果就会很离谱。
我每次思考分治题目的时候都会提醒自己一句话:分治只是手段,合并才是核心。后面的逆序对题目就是这句话最好的证明。
1.2 从“排序数组”到“交易逆序对”的进阶逻辑
这两道题放在一起,因为它们共用同一套分治框架,但难度和技术含量完全不在一个级别。
排序数组(LeetCode 912题)是纯正的归并排序裸题,没有任何变体,只需要你把一个数组排好序就行。它适合用来打基础,验证你对归并排序递归框架的掌握程度。
交易逆序对的总数(剑指Offer 51题)就不一样了。它要求在排序的过程中额外计算一个统计量:数组中满足 i < j 且 nums[i] > nums[j] 的数对个数。直观的暴力解法是双重循环,时间复杂度 O(n^2),数据量一大必然超时。而用归并排序恰好能在合并的过程中自然统计出逆序对数量,时间复杂度直接降到 O(n log n)。
这里有一个很关键的思维跳跃:传统的归并排序只关心“合并后数组是否有序”,而逆序对题目要求你在合并的同时关注“左子数组中哪些元素比右子数组的大”。如果你理解了这个跳跃,就理解了归并排序作为分治算法为什么这么强大——它不仅仅是排序工具,还是一个天然的计数框架。
1.3 这两种解法适合什么场景
我自己的经验是,如果你能把这两道题吃透,你就掌握了分治思想最典型的应用模板。之后你再遇到类似的题目,比如计算重要逆序对、区间和的个数、翻转对等,会发现它们的核心都是“在分治的合并阶段统计某种跨区间的性质”。
所以,这篇内容不会只给你贴代码,我会把每一行代码背后的思考过程、为什么这样写、踩过什么坑都讲清楚。你照着这个思路自己推一遍,比背十道题解都管用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点:排序数组这道题的正确打开方式
2.1 分治模板的三种常见写法
先说明一下,归并排序的实现方式其实有好几种,面试和刷题中常见的分三类:自上而下的递归、自下而上的迭代,以及原地归并。我一个个说。
递归写法是最符合分治思想的写法,代码结构清晰:先处理递归基(数组长度小于等于1直接返回),然后找中点,递归排序左右,最后合并。这种写法的时间复杂度稳定在 O(n log n),空间复杂度 O(n)(因为要用额外的临时数组)。面试中如果你要用归并排序,推荐写这种,因为思路直观,不容易出错。
迭代写法是另一种选择。它从长度为1的子数组开始,两两合并,然后长度翻倍继续,直到整个数组有序。这种写法避免了递归调用带来的栈开销,但在实现时要注意每一轮合并的区间边界,代码写起来比递归更容易出Bug。
原地归并听起来很理想,不需要额外空间,但在数组上实现原地合并的时间开销会变大,因为需要在数组中间反复移动元素,实际生产中很少用,刷题也不推荐。
2.2 排序数组的完整代码拆解
下面这段是我在实际刷题时用的递归版本,注释标注了每一段的关键逻辑:
java复制class Solution {
public int[] sortArray(int[] nums) {
if (nums == null || nums.
