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

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

上节课链接

一.题目

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

二.思路讲解

2.1 逆序对是什么

逆序对 的概念来源于线性代数 ,它用于衡量一个序列的有序程度 。简单来说,在一个数组中,如果存在一对下标 i < jnumsi > numsj ,那么 (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] 范围内就变得有序了。

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

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

相关推荐
wuminyu19 小时前
Java锁机制之park和unpark源码剖析
java·linux·c语言·jvm·c++
梦梦代码精19 小时前
为什么这个开源的AI平台会火?有点东西。。。
人工智能·算法·机器学习·docker·开源
随意起个昵称19 小时前
线性dp-综合刷题1(Not Alone)
算法·动态规划
玖玥拾19 小时前
C/C++ 基础笔记(十一)类的进阶
c语言·c++·设计模式·
-森屿安年-19 小时前
1137. 第 N 个泰波那契数
c++·动态规划
如何原谅奋力过但无声20 小时前
【灵神高频面试题合集09-13】二叉树、二叉搜索树
数据结构·算法·leetcode
程序员老舅20 小时前
从内核视角,看Linux文件读写过程
linux·服务器·c++·内核·linux内核·vfs·linux内存
皆圥忈20 小时前
磁盘物理结构与文件系统基础讲解
linux·算法
数据仓库搬砖人20 小时前
用 LangGraph 从零搭一个客服 Agent:多轮对话 + 工具调用全流程
算法
GuWenyue20 小时前
告别JS类型坑!Ts为什么在ai时代逐渐成为"第一"语言
前端·算法·typescript