https://blog.csdn.net/2601_95366422/article/details/159045777
上节课链接
一.题目
315. 计算右侧小于当前元素的个数 - 力扣(LeetCode)

二.思路讲解
2.1 思路讲解
本题与上一题(逆序对总数)的思路一脉相承 ,本质上就是求每个元素右侧比它小的元素个数 ,即降序合并 视角下的逆序对问题,也就是我们之前提到的**"后面几个比它小"** 的应用。不过,本题的难度在于需要返回一个数组 ,记录每个元素对应的个数,而不仅仅是总数。因此,在统计过程中,我们必须精准地将每个逆序对归因到具体的元素上。
为了实现这一点,我们需要引入一个下标数组 index,用来记录每个元素在原始数组中的位置。在归并排序的过程中,元素的位置会不断变化(因为要排序),但我们需要始终知道当前元素原本是哪个,这样当发现一个逆序对时,才能正确累加到对应的结果位置上。具体做法是:
-
初始化一个与原始数组等长的结果数组 v,全部置为 0。
-
创建一个下标数组
index,初始时index[i] = i,表示第 i 个元素的下标。 -
在进行归并排序时,我们不仅对元素值进行排序,同时也要对下标 进行相应的交换,使得下标与元素始终绑定 。即,当交换两个元素时,它们对应的下标也同步交换。
-
在合并过程中,采用降序合并 的策略,这样当左边元素大于右边元素时,就可以统计右边剩余的元素个数,并累加到左边元素对应的结果中。具体来说:
-
使用指针
cur1和cur2分别遍历左区间和右区间(降序有序)。 -
如果左边元素大于右边,说明左边元素大于右边当前及之后的所有元素(因为右区间降序,右边当前是最大的),那么左边元素对应的结果应加上右边剩余的元素个数**(即
right - cur2 + 1)** ,然后将左边元素放入临时数组,移动cur1。 -
否则,将右边元素放入临时数组,移动
cur2。
-
-
同时,临时数组不仅要存元素值,还要存对应的下标,以保证还原时下标正确。
三.代码演示
cpp
class Solution
{
int tmpNums[500005];//临时数组
int tmpIndex[500005];//临时数组
vector<int>v;//最后要返回的数组
vector<int>index;//对应nums元素的元素下标
public:
vector<int> countSmaller(vector<int>& nums)
{
int n = nums.size();
v.resize(n);
index.resize(n);
for(int i = 0;i < n;i++)
{
index[i] = i;//index1的元素对应的是nums原始下标
}
mermageSort(nums,0,n-1);
return v;
}
void mermageSort(vector<int>& nums,int left,int right)
{
if(left >= right) return;
//1.取中点
int mid = left + (right - left)/2;
//2.左右区间排序
mermageSort(nums,left,mid);
mermageSort(nums,mid+1,right);
//3.合并数组
int cur1 = left,cur2 = mid + 1,i = 0;
while(cur1 <= mid && cur2 <= right)
{
if(nums[cur1] <= nums[cur2])
{
tmpNums[i] = nums[cur2];//更新元素
tmpIndex[i++] = index[cur2++];//更新下标
}
else
{
v[index[cur1]] += right - cur2 + 1 ;//更新结果
tmpNums[i] = nums[cur1];//更新元素
tmpIndex[i++] = index[cur1++];//更新下标
}
}
while(cur1 <= mid)
{
tmpNums[i] = nums[cur1];
tmpIndex[i++] = index[cur1++];
}
while(cur2 <= right)
{
tmpNums[i] = nums[cur2];
tmpIndex[i++] = index[cur2++];
}
//4.还原数组
for(int i = left;i <= right;i++)
{
nums[i] = tmpNums[i - left];
index[i] = tmpIndex[i - left];
}
}
};
四.代码讲解
一、初始化结果数组和下标数组
本题要求返回每个元素右侧比它小的元素个数,因此我们需要一个与原始数组等长的结果数组 v,初始化为全 0 。同时,为了在排序过程中追踪每个元素的原始位置,我们引入一个下标数组 index,初始时 index[i] = i,表示第 i 个元素在原数组中的下标。在归并排序的过程中,元素值会移动,但我们必须保证下标与元素始终绑定,这样当统计逆序对时,才能正确累加到对应的结果位置上。
二、递归函数与临时数组
我们定义一个归并排序函数,参数包括待排序区间 [left, right]。同时,需要准备一个临时数组 tmpNums 用于存储合并过程中的元素值,以及一个临时下标数组 tmpIdx 用于同步存储对应的下标。
三、递归划分与终止条件
-
终止条件 :当
left >= right时,区间内只有一个元素或为空,直接返回。 -
划分中点 :
mid = left + (right - left) / 2,将区间分为左区间[left, mid]和右区间[mid+1, right]。 -
递归排序:先递归处理左区间,再递归处理右区间。递归返回后,左右区间各自内部有序,并且左右区间内部的"右侧小于当前元素"已经统计完毕。
四、合并过程(降序合并)
在合并左右两个有序区间时,我们采用降序合并的策略,即合并后的序列从大到小排列。这样做的目的是为了在比较时方便统计右边比当前元素小的个数。具体步骤如下:
-
指针初始化:
-
cur1指向左区间起点left。 -
cur2指向右区间起点mid + 1。 -
i指向临时数组的起始位置(0)。
-
-
循环比较 :当
cur1 <= mid且cur2 <= right时,比较nums[cur1]和nums[cur2]:-
如果
nums[cur1] > nums[cur2]:说明左区间当前元素大于右区间当前元素。由于右区间是降序,cur2及其后面的所有元素都小于nums[cur1],因此对于左区间这个元素,右侧比它小的元素个数应加上右区间剩余元素的数量,即right - cur2 + 1。将这个数量累加到结果数组中该元素对应的位置 v[index[cur1]]上。然后,将左区间当前元素及其下标存入临时数组,cur1++,i++。 -
否则(
nums[cur1] <= nums[cur2]):此时左区间元素不大于右区间元素,不产生逆序对,直接将右区间当前元素及其下标存入临时数组,cur2++,i++。
-
-
处理剩余元素:
-
如果左区间有剩余,将剩余元素及其下标依次存入临时数组。
-
如果右区间有剩余,同样将剩余元素及其下标存入临时数组。
-
五、将临时数组还原回原数组
合并完成后,临时数组 tmpNums 和 tmpIdx 中存储了当前区间降序排列的元素及其对应的原始下标。我们需要将它们拷贝回原数组 nums 和下标数组 index 的对应位置(从 left 到 right)。这一步保证了在递归返回后,原数组在 [left, right] 范围内是降序的,且下标与元素绑定正确。
六、关键细节
- 下标绑定:在整个过程中,我们必须确保每次交换元素时,对应的下标也随之交换,否则统计会错位。