C++ 分治 归并排序解决问题 力扣 315. 计算右侧小于当前元素的个数 题解 每日一题
文章目录
- 题目描述
- 为什么这道题值得你花几分钟时间看懂?
- 算法原理
- 代码实现
- 核心细节:nums与index的映射关系详解
- 时间复杂度与空间复杂度分析
- 总结
- 下题预告

题目描述
题目链接:力扣 315. 计算右侧小于当前元素的个数
题目描述:
![给你一个整数数组 nums ,按要求返回一个新数组 counts 。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。](https://i-blog.csdnimg.cn/direct/d4924b03312947ccb0fb08716e6b9770.png)
示例 1:
输入:nums = [5,2,6,1]
输出:[2,1,1,0]
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧没有更小的元素
提示:
1 <= nums.length <= 10^5
为什么这道题值得你花几分钟时间看懂?
这道题是归并排序思想的“进阶深化题”,是基于归并排序的通过映射的小技巧来实现我们想要的计数。花时间吃透它,我们将收获远超一道题的价值:
-
深化分治思想的复杂场景应用:这道题要求“按原始位置记录每个元素的右侧小数个数”,需要在归并排序中同时维护“元素值”和“原始索引”的关联,通过这道题我们可以理解分治思想在“带状态跟踪”问题中的应用。
-
掌握“值-索引”映射的核心技巧:当排序操作改变元素位置时,如何通过索引映射追踪原始位置?这道题的核心价值在于教会我们“双数组同步排序”——用一个数组存值、一个数组存对应原始索引,合并时同步操作,确保排序后仍能正确更新原始位置的统计结果。
-
攻克归并排序的状态维护难题:吃透这道题,能彻底搞懂“如何在归并过程中绑定元素值与原始位置”,理解排序操作中“值的比较”与“索引的跟踪”如何分离又同步,让你对归并排序的灵活应用有质的飞跃。
学会它,我们能够对归并排序的理解将从“会用”升级到“活用”~
特别忠告:首先我们一定要先把归并排序的基础逻辑(拆分规则、合并步骤、递归终止条件)完全弄明白,再看这篇博客,如果对归并排序没有思路的朋友可以看下我的这篇博客其中详细讲解了归并排序的实现思路(归并排序 力扣 912. 排序数组),如果有余力的朋友也可以先去看下 力扣 LCR 170 逆序对 这道题(也可以直接看我的博客 归并排序解决问题 力扣 LCR 170. 交易逆序对的总数),这道题是逆序对问题的进阶,需要在理解“分治计数”的基础上,额外处理“索引跟踪”,基础不牢会导致核心逻辑理解断层。如果对归并排序统计逆序对有遗忘,可以回顾相关内容,确保先掌握“拆分-合并-计数”的基本流程。
算法原理
既然都是归并排序解决问题自然实现思路与我们上一篇博客中的逆序对总数统计的解题思路大差不差,归并排序的实现细节在这里就不进行讲解,我们直接说这道题与 逆序对总数统计 不同点进而解决问题。
问题转化:从“统计总数”到“按元素统计”
逆序对总数统计中我们只需要计算“所有 i<j 且 nums[i]>nums[j] 的总对数,而本题要求对每个i,计算j>i 且 nums[j]<nums[i] 的个数。两者的核心差异在于:
- 前者是“全局计数”。
- 后者是“按原始位置的局部计数”。
因此,解法不能仅关注“有多少逆序对”,还要记录“每个原始位置的元素参与了多少逆序对作为左侧元素”。这就需要解决一个关键问题:归并排序会改变元素位置,如何将排序过程中统计的逆序对与原始位置关联起来?
❗❗❗关键不同点:降序排序
和上一道题不同的是这道题因为我们要计的数是这个数右边有多少比我小的数,所以为了能够用计算替遍历我们要用降序来排序,如下图👇:

核心思路:用“索引数组”绑定原始位置
解决位置跟踪问题的关键是引入索引数组(index array),其核心逻辑是:
- 用
nums数组存储元素值,index数组存储对应元素的原始索引(即index[i]表示nums[i]在原始数组中的位置)。 - 归并排序时,同步对
nums和index进行排序——当比较nums[cur1]和nums[cur2]时,交换或移动元素的同时,同步移动index数组中对应的原始索引。 - 这样,即使
nums中的元素位置被排序改变,通过index数组仍能追踪到该元素在原始数组中的位置,从而将统计结果正确记录到ret数组的对应位置。
为什么需要“值-索引”双数组同步排序?
假设原始数组为 [5,2,6,1],其原始索引为 [0,1,2,3]。在归并排序中,当我们比较 5(原始索引 0)和 2(原始索引 1)时,发现 5>2,此时需要统计“5 的右侧有 1 个更小元素”。但如果只排序 nums 数组,会丢失“5 原本在索引 0”的信息,导致无法将结果记录到 ret[0]。
通过 index 数组,当 nums[cur1] 是 5 时,index[cur1] 是 0,我们就能准确地将计数结果累加到 ret[0] 中。这就是“值-索引”双数组的核心作用——在排序改变元素位置时,通过索引数组锚定原始位置。
分治流程:拆分-合并-统计的细化
与逆序对总数统计类似,本题仍采用“拆分-合并”的分治思路,但在合并阶段需要结合索引数组记录每个原始位置的计数:
1.拆分(Divide):将数组拆分为左子数组(left ~ mid)和右子数组(mid+1 ~ right),递归处理左右子数组,确保子数组内部完成排序并统计局部的右侧小数个数。
2.合并(Merge):
准备双指针 cur1(左子数组起始)、cur2(右子数组起始),以及两个临时数组 marknums(存储合并后的元素值)和 markindex(存储合并后元素的原始索引)。
对比 nums[cur1] 和 nums[cur2]:
nums[cur1] > nums[cur2]:
说明左子数组中cur1位置的元素,其右侧(右子数组中cur2及之后的所有元素)都比它小。因此,将右子数组中剩余元素的个数(right - cur2 + 1)累加到ret[index[cur1]]中(通过index[cur1]定位原始位置)。然后将nums[cur1]和index[cur1]分别存入marknums和markindex,cur1后移。- 若
nums[cur1] <= nums[cur2]:无右侧小数,直接将nums[cur2]和index[cur2]存入临时数组,cur2后移。 - 理左右子数组的剩余元素,同步存入临时数组。
- 将临时数组中的结果复制回
nums和index数组,完成合并(确保后续递归层能基于有序数组继续统计)。
- 结果积累:递归过程中,每个元素的右侧小数个数通过
ret数组按原始索引积累,最终ret数组即为答案。
示例推演:以 [5,2,6,1] 为例
这个映射属实是难表达,我尽力画了但是映射的表现效果不是很好/(ㄒoㄒ)/~~,每步结合图片我们着重关注index[cur1]指的是什么可能好看一点,如果哪里有问题欢迎在评论区我们一起讨论
原始数组:nums = [5,2,6,1],index = [0,1,2,3],ret = [0,0,0,0]

第一步:拆分到最小子数组
左子数组 [5,2] 拆分后处理,右子数组 [6,1] 拆分后处理。

第二步:合并左子数组 [5,2]
比较 5(index=0)和 2(index=1):5 > 2,因此 ret[0] += 1(右子数组剩余1个元素)。
合并后 nums 变为 [5,2],index 变为 [0,1](同步记录原始索引)。
第三步:合并右子数组 [6,1]
比较 6(index=2)和 1(index=3):6 > 1,因此 ret[2] += 1。
合并后 nums 变为 [6,1],index 变为 [2,3]。
二三步如下图👇:此时的ret=[1,0,1,0]

第四步:合并左右子数组 [5,2] 和 [6,1]
1.比较 5(index=0)和 6(index=2):5 < 6,此时将6存入临时数组,并将cur2++,i++。

2.比较 5(index=0)和 1(index=3):5 > 1,此时将5存入临时数组,并将cur1++,i++。
此时的ret=[2,0,1,0]

3.比较 2(index=1)和 1(index=3):2 > 1,此时将2存入临时数组,并将cur1++,i++。
此时的ret=[2,1,1,0]

4.因为cur1已经走到边界,所以将cur1中剩余的1(index=3)加入临时数组。

代码实现
class Solution {
public:vector<int> marknums; // 临时存储合并后的元素值vector<int> markindex; // 临时存储合并后元素的原始索引vector<int> index; // 存储当前数组中元素的原始索引(与nums同步排序)vector<int> ret; // 结果数组,按原始索引存储右侧小数个数vector<int> countSmaller(vector<int>& nums) {int n = nums.size();markindex.resize(n);marknums.resize(n);index.resize(n);ret.resize(n, 0); // 初始化结果数组为0// 初始化index数组:index[i] = i,即初始时元素的原始索引为自身位置for (int i = 0; i < n; i++) {index[i] = i;}// 归并排序并统计Msort(nums, 0, n - 1);return ret;}// 归并排序函数:处理nums[left..right],同步维护index数组void Msort(vector<int>& nums, int left, int right) {if (left >= right) return; // 递归终止:子数组长度为1或0// 1. 拆分:递归处理左右子数组int mid = left + (right - left) / 2;Msort(nums, left, mid); // 处理左子数组Msort(nums, mid + 1, right); // 处理右子数组// 2. 合并:统计右侧小数并同步排序nums和indexint cur1 = left; // 左子数组指针int cur2 = mid + 1; // 右子数组指针int i = 0; // 临时数组的索引while (cur1 <= mid && cur2 <= right) {if (nums[cur1] > nums[cur2]) {// 左元素 > 右元素:右子数组中cur2及之后的元素都小于左元素// 通过index[cur1]获取左元素的原始索引,累加计数ret[index[cur1]] += right - cur2 + 1;// 将cur1指向的元素和其原始索引存入临时数组marknums[i] = nums[cur1];markindex[i++] = index[cur1++];} else {// 左元素 <= 右元素:无右侧小数,存入cur2指向的元素及其原始索引marknums[i] = nums[cur2];markindex[i++] = index[cur2++];}}// 处理左子数组剩余元素while (cur1 <= mid) {marknums[i] = nums[cur1];markindex[i++] = index[cur1++];}// 处理右子数组剩余元素while (cur2 <= right) {marknums[i] = nums[cur2];markindex[i++] = index[cur2++];}// 将合并后的结果复制回原数组,确保nums和index同步更新for (int k = left; k <= right; k++) {nums[k] = marknums[k - left]; // 恢复元素值index[k] = markindex[k - left]; // 恢复对应原始索引}}
};
核心细节:nums与index的映射关系详解
代码中 nums 和 index 是一一对应的平行数组,其映射关系是解题的核心,我们必须明确:
nums[i]表示当前位置的元素值(排序过程中会被移动)。index[i]表示nums[i]在原始数组中的位置(例如,若原始数组中索引 0 的元素被移动到位置 i,则index[i] = 0)。
同步排序的意义:
- 当我们比较
nums[cur1]和nums[cur2]时,实际是在比较两个元素的值大小。 - 当决定移动
cur1对应的元素时,必须同步移动index[cur1],才能保证markindex中存储的原始索引与marknums中的元素值对应。 - 最终将
marknums和markindex复制回nums和index时,也是为了让下一层递归能基于最新的排序结果继续比较,同时保持值与原始索引的绑定。
为什么要维护这种映射?
- 统计结果
ret必须按原始索引存储(例如原始数组中索引 0 的元素,其结果要存在ret[0])。 - 归并排序会改变元素在
nums中的位置,但index数组始终记录着“这个元素原本在原始数组的哪里”,因此才能通过ret[index[cur1]]正确累加计数。
时间复杂度与空间复杂度分析
- 时间复杂度:O(nlogn)。数组拆分的深度为 logn,每一层的合并与计数操作遍历当前层所有元素(O(n)),因此总时间复杂度为 O(nlogn),适合 n=10^5 的数据规模。
- 空间复杂度:O(n)。需要存储 4 个数组(
marknums、markindex、index、ret),每个数组的长度均为 n,因此总空间复杂度为 O(n)。
总结
- 核心思路:在归并排序的“拆分-合并”框架中,通过“值-索引”双数组同步排序,追踪元素的原始位置,将每个元素的右侧小数个数按原始索引记录到结果数组中。
- 关键理解:
nums数组存储排序过程中的元素值,index数组同步存储对应元素的原始索引,两者的绑定确保排序后仍能正确关联原始位置与统计结果。 - 操作要点:合并阶段比较元素值时,通过
index数组获取原始索引,将右子数组中满足条件的元素个数累加到结果数组的对应位置。 - 迁移价值:这种“双数组同步维护状态”的思路,可迁移到所有需要“排序+原始信息跟踪”的问题中,例如统计“每个元素左侧大于它的元素个数”等,是分治思想与状态维护结合的典型范例。
下题预告
下一篇我们将聚焦 力扣 493. 反转对——这道题是归并排序思想的“升级挑战场”!它要求统计“i<j 且 nums[i] > 2*nums[j]”的逆序对,不仅需要维护元素的位置关系,还需处理更复杂的数值比较逻辑。刚掌握“值-索引”映射的核心技巧,正好用它来深化实战,进一步理解分治思想在复杂计数问题中的灵活应用,打通从“简单逆序对”到“带倍数关系逆序对”的解题链路。
Doro 带着小花🌸来啦!🌸奖励🌸看到这里的你!如果这篇内容帮你理清了映射的核心逻辑,或是让你对归并排序的状态维护有了更清晰的认识,别忘了点赞支持呀!把它收藏起来,以后复习这类问题时翻出来,就能快速回忆起关键细节~关注我,我会持续更新算法系列内容,有什么解题思路或疑问我们随时讨论,算法专辑里还有更多经典题目等着你解锁!

