https://blog.csdn.net/2601_95366422/article/details/159044781
上节课链接
一.题目
LCR 170. 交易逆序对的总数 - 力扣(LeetCode)

二.思路讲解
2.1 逆序对是什么
逆序对 的概念来源于线性代数 ,它用于衡量一个序列的有序程度 。简单来说,在一个数组中,如果存在一对下标 i < j 且 nums[i] > nums[j] ,那么 (i, j) 就构成一个逆序对。统计逆序对的数量有两种常用视角:
-
固定当前元素,看它前面有多少个比它大的元素,将这些个数累加。
-
固定当前元素,看它后面有多少个比它小的元素,同样累加。
这两种视角是等价的,可以根据具体算法选择方便的一种。理解逆序对是解决很多排序相关问题的基础。
2.2 逆序对和归并如何结合
根据前面几个比它大(升序合并)
当我们将两个有序子数组合并成升序序列时,可以使用固定右区间元素,看左区间有多少比它大的思路。具体来说:
-
假设当前合并的是左区间 [l, mid] 和右区间 [mid+1, r],且两个区间都已升序。
-
用指针
cur1遍历左区间,cur2遍历右区间。在合并过程中,如果发现nums[cur1] > nums[cur2],那么由于左区间是升序,cur1及其后面的所有元素都大于nums[cur2]。因此,对于当前右区间的这个元素,左区间中从cur1到mid的所有元素都与它构成逆序对,个数为mid - cur1 + 1。然后我们将较小的nums[cur2]放入临时数组,并移动cur2。 -
如果
nums[cur1] <= nums[cur2],则没有逆序对,直接将nums[cur1]放入临时数组,移动cur1。
这样,在合并的同时就统计出了所有跨越左右区间的逆序对。而左区间内部和右区间内部的逆序对已在递归中统计完毕。这种方法的关键在于利用升序性质,通过一次比较得到多个逆序对。
根据后面几个比它小(降序合并)
如果我们将两个有序子数组合并成降序 序列,则可以使用固定左区间元素,看右区间有多少比它小的思路。具体来说:
-
假设当前合并的是左区间 [l, mid] 和右区间 [mid+1, r] ,且两个区间都已降序。
-
用指针
cur1遍历左区间,cur2遍历右区间。在合并过程中,如果发现nums[cur1] > nums[cur2],那么由于右区间是降序,cur2及其后面的所有元素都小于nums[cur1](因为降序排列,越往后越小)。因此,对于当前左区间的这个元素,右区间中从cur2到r的所有元素都与它构成逆序对,个数为r - cur2 + 1。然后我们将较大的nums[cur1]放入临时数组,并移动cur1。 -
如果
nums[cur1] <= nums[cur2],则没有逆序对,直接将nums[cur2]放入临时数组,移动cur2。这种方式同样可以批量统计逆序对,且与升序合并对称。两种方法本质相同,只是视角不同,实际应用中可以根据需要选择。
三.代码演示
3.1 升序合并
cpp
class Solution
{
vector<int> tmp;
int ret = 0;
public:
int reversePairs(vector<int>& record)
{
int n = record.size();
tmp.resize(n);
mermageSort(record,0,n-1);
return ret;
}
void mermageSort(vector<int>& record,int left,int right)
{
if(left >= right) return;
//1.找中间点
int mid = left + (right - left)/2;
//2.左右区间排序
mermageSort(record,left,mid);
mermageSort(record,mid+1,right);
//3.合并数组升序
int cur1 = left,cur2 = mid + 1,i = 0;
while(cur1 <= mid && cur2 <= right)
{
if(record[cur1] <= record[cur2])
{
tmp[i++] = record[cur1++];
}
else
{
ret += mid - cur1 + 1;//cur-mid都是大于record[cur2]的
tmp[i++] = record[cur2++];
}
}
while(cur1 <= mid) tmp[i++] = record[cur1++];
while(cur2 <= right) tmp[i++] = record[cur2++];
//4.还原数组
for(int i = left;i <= right;i++)
{
record[i] = tmp[i - left];
}
}
};
3.2 降序合并
cpp
class Solution
{
vector<int> tmp;
int ret = 0;
public:
int reversePairs(vector<int>& record)
{
int n = record.size();
tmp.resize(n);
mermageSort(record,0,n-1);
return ret;
}
void mermageSort(vector<int>& record,int left,int right)
{
if(left >= right) return;
//1.找中间点
int mid = left + (right - left)/2;
//2.左右区间排序
mermageSort(record,left,mid);
mermageSort(record,mid+1,right);
//3.合并数组降序
int cur1 = left,cur2 = mid + 1,i = 0;
while(cur1 <= mid && cur2 <= right)
{
if(record[cur1] <= record[cur2])
{
tmp[i++] = record[cur2++];
}
else
{
ret += right - cur2 + 1;//right - cur都是小于record[cur1]的
tmp[i++] = record[cur1++];
}
}
while(cur1 <= mid) tmp[i++] = record[cur1++];
while(cur2 <= right) tmp[i++] = record[cur2++];
//4.还原数组
for(int i = left;i <= right;i++)
{
record[i] = tmp[i - left];
}
}
};
四.代码讲解
一、初始化临时数组与结果变量
在开始排序前,我们需要一个临时数组 tmp 来辅助合并操作,同时定义一个全局变量 ret 用于累计逆序对的总数。在 reversePairs 函数中,首先获取数组长度 n,将 tmp 的大小调整为 n,然后调用归并排序函数 mermageSort 对数组进行排序并统计逆序对。最终返回 ret。
二、递归函数与终止条件
mermageSort 函数接收当前待处理区间的左右边界 left 和 right(闭区间)。当 left >= right 时,说明区间内只有一个元素或为空,不存在逆序对,直接返回。这是递归的终止条件。
三、找中间点,划分左右区间
为了分治,我们需要将当前区间一分为二。计算中间位置 mid = left + (right - left) / 2,这样就将区间划分为 左区间 [left, mid] 和 右区间 [mid + 1, right]。这种写法可以防止整数溢出。
四、递归排序左右区间
分别对左区间和右区间调用 mermageSort 进行递归排序。这一步保证了当递归返回时,左区间和右区间各自内部已经是有序的,同时它们内部的逆序对已经在递归过程中被统计并累加到 ret 中。
五、合并两个有序区间并统计逆序对
当左右区间都有序后,我们需要将它们合并成一个大的有序区间(这里采用升序合并),并在合并过程中统计跨越左右区间的逆序对。定义三个指针:
-
cur1指向左区间的起始位置left -
cur2指向右区间的起始位置mid + 1 -
i指向临时数组tmp的起始位置(从0开始)
然后进入循环,条件为 cur1 <= mid && cur2 <= right,即两个区间都还有元素未处理。在循环中,比较 record[cur1] 和 record[cur2]:
-
如果
record[cur1] <= record[cur2]:说明左边元素小于等于右边,此时不构成逆序对(因为左边在前,且值更小)。将左边元素放入临时数组,然后cur1++,i++。 -
如果
record[cur1] > record[cur2]:说明左边元素大于右边,**此时对于右边这个元素,左区间中从cur1到mid的所有元素都大于它(因为左区间是升序),**因此这些元素都与当前右边元素构成逆序对, 个数为mid - cur1 + 1。将这个数量累加到ret中,然后将右边元素放入临时数组,cur2++,i++。
当其中一个区间遍历完后,将另一个区间剩余的元素全部放入临时数组(这些剩余元素不会产生新的跨越逆序对,因为已经比较过了)。
六、将临时数组还原回原数组
合并完成后,tmp 的 [0, i-1] 区间内存储了当前 [left, right] 范围内的所有有序元素。现在需要将这些元素拷贝回原数组 的对应位置。用一个循环,从 left 到 right,将 tmp[i-left] 赋值给 record[i]。这样,原数组在 [left, right] 范围内就变得有序了。
七、关键细节
-
临时数组的作用:避免了在每次合并时创建新数组,节省了时间和空间开销。
-
统计时机:逆序对的统计只在合并过程中进行,利用了两个子数组已经有序的特性,通过一次比较就能得到多个逆序对,大大提高了效率。