分治(交易逆序对的总数)(6)

https://blog.csdn.net/2601_95366422/article/details/159044781

上节课链接

一.题目

LCR 170. 交易逆序对的总数 - 力扣(LeetCode)

二.思路讲解

2.1 逆序对是什么

逆序对 的概念来源于线性代数 ,它用于衡量一个序列的有序程度 。简单来说,在一个数组中,如果存在一对下标 i < jnums[i] > nums[j] ,那么 (i, j) 就构成一个逆序对。统计逆序对的数量有两种常用视角:

  • 固定当前元素,看它前面有多少个比它大的元素,将这些个数累加。

  • 固定当前元素,看它后面有多少个比它小的元素,同样累加。

这两种视角是等价的,可以根据具体算法选择方便的一种。理解逆序对是解决很多排序相关问题的基础

2.2 逆序对和归并如何结合

根据前面几个比它大(升序合并)

当我们将两个有序子数组合并成升序序列时,可以使用固定右区间元素,看左区间有多少比它大的思路。具体来说:

  • 假设当前合并的是左区间 [l, mid] 和右区间 [mid+1, r],且两个区间都已升序。

  • 用指针 cur1 遍历左区间,cur2 遍历右区间。在合并过程中,如果发现 nums[cur1] > nums[cur2] ,那么由于左区间是升序,cur1 及其后面的所有元素都大于 nums[cur2]。因此,对于当前右区间的这个元素,左区间中从 cur1mid 的所有元素都与它构成逆序对,个数为 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](因为降序排列,越往后越小)。因此,对于当前左区间的这个元素,右区间中从 cur2r 的所有元素都与它构成逆序对,个数为 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 函数接收当前待处理区间的左右边界 leftright(闭区间)。当 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] :说明左边元素大于右边,**此时对于右边这个元素,左区间中从 cur1mid 的所有元素都大于它(因为左区间是升序),**因此这些元素都与当前右边元素构成逆序对, 个数为 mid - cur1 + 1 。将这个数量累加到 ret 中,然后将右边元素放入临时数组,cur2++i++

当其中一个区间遍历完后,将另一个区间剩余的元素全部放入临时数组(这些剩余元素不会产生新的跨越逆序对,因为已经比较过了)

六、将临时数组还原回原数组

合并完成后,tmp[0, i-1] 区间内存储了当前 [left, right] 范围内的所有有序元素。现在需要将这些元素拷贝回原数组 的对应位置。用一个循环,从 leftright,将 tmp[i-left] 赋值给 record[i]。这样,原数组在 [left, right] 范围内就变得有序了。

七、关键细节
  • 临时数组的作用:避免了在每次合并时创建新数组,节省了时间和空间开销。

  • 统计时机:逆序对的统计只在合并过程中进行,利用了两个子数组已经有序的特性,通过一次比较就能得到多个逆序对,大大提高了效率。

相关推荐
北顾笙9801 小时前
day14-数据结构力扣
数据结构·算法·leetcode
郝学胜-神的一滴2 小时前
[简化版 GAMES 101] 计算机图形学 03:线性代数下
开发语言·c++·线性代数·图形渲染
Ln5x9qZC22 小时前
尾递归与Continuation
算法
一路向北he2 小时前
esp32库依赖
c语言·c++·算法
老四啊laosi2 小时前
[双指针] 6. 查找总价为目标值的两个商品
算法·力扣·总价为目标值得两商品
Howrun7772 小时前
C++ 项目测试全指南:从 0 基础到落地实操
开发语言·c++·log4j
YYYing.2 小时前
【Linux/C++网络篇(二) 】TCP并发服务器演进史:从多进程到Epoll的进化指南
linux·服务器·网络·c++·tcp/ip
追光的蜗牛丿2 小时前
C++传递参数时什么情况下传递引用
开发语言·javascript·c++
sheng42042 小时前
小记近期C++遇到的坑
c++