当前位置: 首页 > article >正文

分治算法(6)_归并排序_交易逆序对的总数

个人主页:C++忠实粉丝
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创

分治算法(6)_归并排序_交易逆序对的总数

收录于专栏【经典算法练习】
本专栏旨在分享学习算法的一点学习笔记,欢迎大家在评论区交流讨论💌

目录

温馨提示: 

1. 题目链接:

2. 题目描述:

3. 解法:

算法思路:

方法一:快速统计出某个数前面有多少个数比它大(升序)

方法二:快速统计出某个数后面有多少个数比它小(降序)

代码展示:

结果分析:

 


温馨提示: 

这里需要有归并排序的相关知识, 如果对归并排序还不是很了解的宝子们, 可以先去下方链接进行查看:

分治算法(5)_归并排序_排序数组-CSDN博客 

1. 题目链接:

OJ链接 :  交易逆序对的总数

2. 题目描述:

在股票交易中,如果前一天的股价高于后一天的股价,则可以认为存在一个「交易逆序对」。请设计一个程序,输入一段时间内的股票交易记录 record,返回其中存在的「交易逆序对」总数。

示例 1:

输入:record = [9, 7, 5, 4, 6]
输出:8
解释:交易中的逆序对为 (9, 7), (9, 5), (9, 4), (9, 6), (7, 5), (7, 4), (7, 6), (5, 4)。

限制:

0 <= record.length <= 50000

3. 解法:

算法思路:

用归并排序求逆序数是很经典的方法, 主要是在归并排序的合并过程中统计出逆序对的数量, 也就是在合并两个有序序列的过程中, 能够快速求出逆序对的数量.

 我们将这个问题分解成几个小问题, 逐一破解这道题.

为什么可以使用归并排序?

如果我们将数组从中间划分成两个部分, 那么我们可以将逆序对产生的方式划分成三组:

1. 逆序对中两个元素: 全部从左数组中选择

2. 逆序对中两个元素: 全部从右数组中选择

3. 逆序对中两个元素: 一个选左数组另一个选右数组 

根据排列组合的分类相加原理, 三种情况下产生的逆序对的总和, 正好等于总的逆序对数量. 

示例: 

因此, 我们可以利用对并排序的过程, 先求出左半数组中逆序对的数量, 再求出右半数组中逆序对的数量, 最后求出一个选择左边, 另一个选择右边情况下逆序对的数量. 三者相加即可.

为什么要这么做? 

在归并排序合并过程中,  我们得到的是两个有序的数组. 我们是可以利用数组的有序性, 快速统计出逆序对的数量, 而不是将所有情况都枚举出来.

最核心的问题, 如何在合并两个有序数组的过程中, 统计出逆序对的数量?

合并两个有序序列时求逆序对的方法有两种:

1. 快速统计出某个数前面有多少个数比它大

2. 快速统计出某个数后面有多少数比它小

方法一:快速统计出某个数前面有多少个数比它大(升序)

通过一个示例来演示方法一:

假定已经有两个已经有序的序列以及辅助数组 left = [5, 7, 9] right = [4, 5, 8] help = [],通过合并两
个有序数组的过程,来求得逆序对的数量:
规定如下定义来叙述过程 :
cur1 遍历 left 数组,cur2 遍历 right 数组
ret 记录逆序对的数量

第⼀轮循环:
left[cur1] > right[cur2],由于两个数组都是升序的,那么我们可以断定,此刻 left 数组中
[cur1, 2] 区间内的 3 个元素均可与 right[cur2] 的元素构成逆序对,因此可以累加逆序对的数量 ret
+= 3,并且将 right[cur2] 加⼊到辅助数组中,cur2++ 遍历下⼀个元素。
第⼀轮循环结束后:left = [5, 7, 9] right = [x, 5, 8] help = [4] ret = 3 cur1 = 0 cur2 = 1

第⼆轮循环:
left[cur1] == right[cur2],因为 right[cur2] 可能与 left 数组中往后的元素构成逆序对,因此我
们需要将 left[cur1] 加⼊到辅助数组中去,此时没有产生逆序对,不更新 ret。
第⼆轮循环结束后:left = [x, 7, 9] right = [x, 5, 8] help = [4, 5] ret = 3 cur1 = 1 cur2 = 1

第三轮循环:
left[cur1] > right[cur2],与第⼀轮循环相同,此刻 left 数组中[cur1, 2] 区间内的 2 个元素均可
与 right[cur2] 的元素构成逆序对,更新 ret 的值为 ret += 2,并且将 right[cur2] 加⼊到辅助数组中
去,cur2++ 遍历下⼀个元素。
第三轮循环结束后:left = [x, 7, 9] right = [x, x, 8] help = [4, 5, 5] ret = 5 cur1 = 1 cur2 = 2

第四轮循环:
left[cur1] < right[cur2],由于两个数组都是升序的,因此我们可以确定 left[cur1] 比 right 数
组中的所有元素都要小。left[cur1] 这个元素是不可能与 right 数组中的元素构成逆序对。因此,⼤胆的将 left[cur1] 这个元素加入到辅助数组中去,不更细 ret 的值。
第四轮循环结束后:left = [x, x, 9] right = [x, x, 8] help = [4, 5, 5, 7] ret = 5 cur1 = 2 cur2 = 2

第五轮循环:
left[cur1] > right[cur2],与第⼀、第三轮循环相同。此时 left 数组内的 1 个元素能与
right[cur2] 构成逆序对,更新 ret 的值,并且将 right[cur2] 加⼊到辅助数组中去。
第五轮循环结束后:left = [x, x, 9] right = [x, x, x] help = [4, 5, 5, 7, 8] ret = 6 cur1 = 2 cur2 = 2


处理剩余元素:
    • 如果是左边出现剩余,说明左边剩下的所有元素都是比右边元素⼤的,但是它们都是已经被计算过的(我们以右边的元素为基准的),因此不会产生逆序对,仅需归并排序即可。
    • 如果是右边出现剩余,说明右边剩下的元素都是比左边大的,不符合逆序对的定义,因此也不需要处理,仅需归并排序即可
    

整个过程只需将两个数组遍历⼀遍即可,时间复杂度为 O(N)。
由上述过程我们可以得出方法⼀统计逆序对的关键点:
在合并有序数组的时候,遇到左数组当前元素 > 右数组当前元素时,我们可以通过计算左数组中剩余元素的长度,就可快速求出右数组当前元素前面有多少个数比它大,对比解法一中一个一个枚举逆序对效率快了许多。

 

 

方法二:快速统计出某个数后面有多少个数比它小(降序)

依旧通过一个示例来演示方法二:
假定已经有两个已经有序的序列以及辅助数组 left = [5, 7, 9] right = [4, 5, 8] help = [],通过合并两
个有序数组的过程,来求得逆序对的数量:
规定如下定义来叙述过程 :
cur1 遍历 left 数组,cur2 遍历 right 数组
ret 记录逆序对的数量

第⼀轮循环:
left[cur1] > right[cur2],先不要着急统计,因为我们要找的是当前元素后⾯有多少比它小的,
这里虽然出现了一个,但是 right 数组中依旧还可能有其余比它小的。因此此时仅将 right[cur2] 加⼊
到辅助数组中去,并且将 cur2++。
第⼀轮循环结束后:left = [5, 7, 9] right = [x, 5, 8] help = [4] ret = 0 cur1 = 0 cur2 = 1
第⼆轮循环:
left[cur1] == right[cur2],由于两个数组都是升序,这个时候对于元素 left[cur1] 来说,我们已
经可以断定 right 数组中[0, cur2) 左闭右开区间上的元素都是比它小的。因此此时可以统计逆序对的数量 ret += cur2 - 0,并且将 left[cur1] 放⼊到辅助数组中去,cur1++ 遍历下⼀个元素。
第二轮循环结束后:left = [x, 7, 9] right = [x, 5, 8] help = [4, 5] ret = 1 cur1 = 1 cur2 = 1

第三轮循环:
left[cur1] > right[cur2],与第⼀轮循环相同,直接将 right[cur2] 加⼊到辅助数组中去,
cur2++ 遍历下⼀个元素。
第三轮循环结束后:left = [x, 7, 9] right = [x, x, 8] help = [4, 5, 5] ret = 1 cur1 = 1 cur2 = 2

第四轮循环:
left[cur1] < right[cur2],由于两个数组都是升序的,这个时候对于元素 left[cur1] 来说,我们
依旧已经可以断定 right 数组中[0, cur2) 左闭右开区间上的元素都是比它小的。因此此时可以统计逆序对的数量 ret += cur2 - 0,并且将 left[cur1] 放⼊到辅助数组中去,cur1++ 遍历下⼀个元素。
第四轮循环结束后:left = [9] right = [8] help = [4, 5, 5, 7] ret = 3 cur1 = 2 cur2 = 2

第五轮循环:
left[cur1] > right[cur2],与第⼀、第三轮循环相同。直接将 right[cur2] 加⼊到辅助数组中去,
cur2++ 遍历下⼀个元素。
第五轮循环结束后:left = [x, x, 9] right = [x, x, x] help = [4, 5, 5, 7, 8] ret = 3 cur1 = 2 cur2 = 2

处理剩余元素:
• 如果是左边出现剩余,说明左边剩下的所有元素都是比右边元素大的,但是相比较于方法一,逆序对的数量是没有统计过的。因此,我们需要统计 ret 的值:
◦ 设左边数组剩余元素的个数为 leave
◦ ret += leave * (cur2 - 0)
对于本题来说,处理剩余元素的时候, left 数组剩余 1 个元素,cur2 - 0 = 3,因此 ret 需要类加
上 3,结果为 6。与方法一求得的结果相同。
• 如果是右边出现剩余,说明右边剩下的元素都是⽐左边⼤的,不符合逆序对的定义,因此也不需要处理,仅需归并排序即可。

整个过程只需将两个数组遍历⼀遍即可,时间复杂度依旧为 O(N)。

由上述过程我们可以得出方法二统计逆序对的关键点:
在合并有序数组的时候,遇到左数组当前元素 <= 右数组当前元素时,我们可以通过计算右数组已经遍历过的元素的长度,快速求出左数组当前元素后面有多少个数比它大。
但是需要注意的是,在处理剩余元素的时候,方法二还需要统计逆序对的数量。

 

代码展示:

升序:

class Solution 
{
    int tmp[50010];
public:
    int reversePairs(vector<int>& record) {
        return mergesort(record, 0, record.size() - 1);
    }

    int mergesort(vector<int>& nums, int left, int right)
    {
        if(left >= right) return 0;

        int ret = 0;
        //找中间点, 将数组分成两部分
        int mid = (left + right) >> 1;
        
        //左边的个数 + 排序 + 右边的个数 + 排序
        ret += mergesort(nums, left, mid) + mergesort(nums, mid + 1, right);
        
        //一左一右的个数
        int cur1 = left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid && cur2 <= right)
        {
            if(nums[cur1] <= nums[cur2]) tmp[i++] = nums[cur1++]; 
            else ret += mid - cur1 + 1, tmp[i++] = nums[cur2++];
        }

        //处理排序
        while(cur1 <= mid) tmp[i++] = nums[cur1++];
        while(cur2 <= right) tmp[i++] = nums[cur2++];
        for(int j = left; j <= right; j++)
            nums[j] = tmp[j - left];

        return ret;
    }
};

 

降序:

class Solution 
{
    int tmp[50010];
public:
    int reversePairs(vector<int>& record) {
        return mergesort(record, 0, record.size() - 1);
    }

    int mergesort(vector<int>& nums, int left, int right)
    {
        if(left >= right) return 0;

        int ret = 0;
        //找中间点, 将数组分成两部分
        int mid = (left + right) >> 1;
        
        //左边的个数 + 排序 + 右边的个数 + 排序
        ret += mergesort(nums, left, mid) + mergesort(nums, mid + 1, right);
        
        //一左一右的个数
        int cur1 = left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid && cur2 <= right)
        {
            if(nums[cur1] <= nums[cur2]) tmp[i++] = nums[cur2++]; 
            else ret += right - cur2 + 1, tmp[i++] = nums[cur1++];
        }

        //处理排序
        while(cur1 <= mid) tmp[i++] = nums[cur1++];
        while(cur2 <= right) tmp[i++] = nums[cur2++];
        for(int j = left; j <= right; j++)
            nums[j] = tmp[j - left];

        return ret;
    }
};

 

结果分析:


http://www.kler.cn/news/340209.html

相关文章:

  • 灵芝玉叶膏简介
  • 影刀RPA实战:Excel密码与字典功能指令
  • InfoGAN:通过信息最大化生成对抗网络进行可解释的表示学习
  • React 插入不转义的html
  • Python 语法及入门(超全超详细)!
  • nacos多数据源插件介绍以及使用
  • dnf进程CPU使用率高,dnf和yum命令卡住,无法退出
  • 数字码头APP会员端功能模块化设计
  • 《C++职场中,如何塑造卓越的技术领导力》
  • 一台电脑轻松接入CANFD总线_来可CNA板卡介绍
  • AI绘画:人工智能颠覆艺术创作的新时代
  • 银河麒麟V10中启用SELinux
  • 测试--Tpshop商城
  • Python常用函数集锦
  • 10款视频制作软件推荐:制作视频的速成工具
  • Android 安装过程五 MSG_INSTALL消息的处理 安装
  • 宠物咖啡馆业务自动化:SpringBoot框架的实现方法
  • 智能指针(2)
  • 登陆状态检测设计:Vue3+TypeScript+JWT+SpringSecurity+Redis+SpringBoot+Axios二次封装
  • Vue入门-第一个Demo