算法:分治(归并)题目练习

news2024/11/29 4:49:40

目录

题目一:排序数组

题目二:数组中的逆序对

题目三:计算右侧小于当前元素的个数

题目四:翻转对


题目一:排序数组

给你一个整数数组 nums,请你将该数组升序排列。

示例 1:

输入:nums = [5,2,3,1]
输出:[1,2,3,5]

示例 2:

输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]

归并排序也就是找一个中间值mid,先排mid左边的区域,再排mid右边的区域,而左边的区域又可以分为两块,继续排序,以此类推 .....最后再将两个有序数组进行合并

这里的归并排序与上一章节的快排,都是执行了数组分块的逻辑,较为类似,所以放在一块讲解

这里可以将快排理解为二叉树的前序遍历,即先将根结点这一层分好,再去分左子树,右子树...

而归并排序,可以理解为二叉树的后序遍历,即先将左子树、右子树分完,再合并到根结点处

这里的归并排序同样分为下面几步:

①选择中间点mid
②左右区间排序
③合并左右两个数组
④将 tmp 数组数据还原到数组 nums 中


代码如下:

class Solution 
{
    vector<int> tmp;//临时数组放全局,相比于放局部提高效率
public:
    vector<int> sortArray(vector<int>& nums) 
    {
        tmp.resize(nums.size());//临时数组 开空间 + 初始化
        mergeSort(nums, 0, nums.size() - 1);
        return nums;
    }

    void mergeSort(vector<int>& nums, int left, int right)
    {
        if(left >= right) return;
        //选择中间点划分
        int mid = ((right - left) >> 1) + left;
        //左右区间排序
        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);
        //合并两个数组
        int cur1 = left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid && cur2 <= right)
            tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur1++] : nums[cur2++];
        //处理两个数组中可能有剩余元素的数组,即没有遍历完的数组
        while(cur1 <= mid) tmp[i++] = nums[cur1++]; 
        while(cur2 <= right) tmp[i++] = nums[cur2++];
        //还原数组
        for(int i = left; i <= right; i++) nums[i] = tmp[i - left];
    }
};

题目二:数组中的逆序对

在股票交易中,如果前一天的股价高于后一天的股价,则可以认为存在一个「交易逆序对」。请设计一个程序,输入一段时间内的股票交易记录 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)。

逆序对其实在大学的课程是学过的,只要在数组中从前往后挑选的两个数中,前一个数比后一个数大,就称这两个数为一个逆序对

解法一:暴力解法

两层for循环,依次枚举每一个组合,从所有组合中寻找逆序对,这种解法比较简单,且一定会超时,所以代码就不列举了

解法二:归并排序

下面可以对归并排序说明,即分为两部分,左边部分挑出来逆序对个数,右边部分挑出来逆序对个数,再一左一右挑出来逆序对个数,相加即可

下面可以优化为:

左半部分 -> 左排序 -> 右半部分 -> 右排序 -> 一左一右 -> 排序

策略一:找出该数之前,有多少个数比我大的(升序排列)

现在分为两部分,如下所示:

因为是升序排序的,所以此时cur1、cur2前面的部分都是小的,由于要找的逆序对时前面的数比后面的数大,所以当找到cur1大于cur2的时候,左半部分的cur1后面的其他数也肯定大于cur2,此时就不用继续比较了,直接加上后面的个数,再cur2++,往后寻找,也就是:

①nums[cur1] <= nums[cur2]:cur1++
②nums[cur] > nums[cur2]:ret += mid - cur1 + 1,cur2++


策略二:找出该数之后,有多少个数比我小的(降序排列)

降序排列如下所示:

①nums[cur2] < nums[cur1]:ret += right - (mid + 1) +1 = right - mid,cur1++
②nums[cur2] >= nums[cur1]:cur2++


代码如下:

class Solution 
{
    vector<int> tmp;
public:
    int reversePairs(vector<int>& record) 
    {
        tmp.resize(record.size());
        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 = ((right - left) >> 1) + left;
        //左边数组 + 排序 + 右边数组 + 排序
        ret += mergeSort(nums, left, mid);
        ret += 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 && cur2 <= right)
        // {
        //     if(nums[cur1] > nums[cur2])
        //     {
        //         ret += right - cur2 + 1;
        //         tmp[i++] = nums[cur1++];
        //     }
        //     else
        //     {
        //         tmp[i++] = nums[cur2++];
        //     }
        // }
        while(cur1 <= mid) tmp[i++] = nums[cur1++];
        while(cur2 <= right) tmp[i++] = nums[cur2++];
        //还原数组
        for(i = left; i <= right; i++) nums[i] = tmp[i - left];
        return ret;
    }
};

题目三:计算右侧小于当前元素的个数

给你一个整数数组 nums ,按要求返回一个新数组 counts 。数组 counts 有该性质: counts[i] 的值是  nums[i] 右侧小于 nums[i] 的元素的数量。

示例 1:

输入:nums = [5,2,6,1]
输出:[2,1,1,0] 
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素

示例 2:

输入:nums = [-1]
输出:[0]

示例 3:

输入:nums = [-1,-1]
输出:[0,0]

这道题与上一道题的逆序对比较像,上一题是直接求逆序对的数量,而此题是求每个元素有几个右边比它小的元素的数量

解法:归并排序(分治)

上一题讲到了有两个策略:

策略一:求一个数左边有多少个数比我大(升序)

策略二:求一个数右边有多少个数比我小(降序)

此题使用策略二比较方便些,图如下所示:

所以就有下面两种情况:

①nums[cur1] <= nums[cur2]:cur2++
②nums[cur1] > nums[cur2]:数组中cur1这个位置的数 = 原始下标 + right - cur2 + 1

那么由于归并排序是会改变数组中元素位置的,怎么能够记住数组中原始下标的位置呢?

利用哈希的思想, 创建一个和nums等规模大小的数组,并记录原始的下标,在nums数组的元素移动时,该数组也跟着移动即可

由于此时多了一个index下标数组,所以还需要多创建一个辅助数组,需要注意的是在使用nums数组的辅助数组tmpNums时,index数组的辅助数组tmpIndex也需要使用,为了最终能够找到原始下标

在处理一左一右两部分时,由于需要记录cur1位置的数值,那么就需要该位置的数值所对应的原始下标,原始下标为index[cur1],找到了原始下标,那么此时往最终的数组中写时,就变为了:
ret[index[cur1]]


代码如下:

class Solution 
{
    vector<int> ret;      //最终结果
    vector<int> index;    //原始下标
    int tmpNums[500010];  //辅助数组
    int tmpIndex[500010]; //辅助数组
public:
    vector<int> countSmaller(vector<int>& nums) 
    {
        int n = nums.size();
        ret.resize(n), index.resize(n); 
        //初始化 index 数组
        for(int i = 0; i < n; i++) index[i] = i;
        mergeSort(nums, 0, nums.size() - 1);
        return ret;
    }

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

        int mid = ((right - left) >> 1) + left;
        //先处理左右两部分
        //[left, mid] [mid + 1, right]
        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])
            {
                tmpNums[i] = nums[cur2];
                tmpIndex[i++] = index[cur2++];
            }
            else
            {
                ret[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++];
        }
        //还原数组
        for(int i = left; i <= right; i++)
        {
            nums[i] = tmpNums[i - left];
            index[i] = tmpIndex[i - left];
        }
    }
};

题目四:翻转对

给定一个数组 nums ,如果 i < j 且 nums[i] > 2*nums[j] 我们就将 (i, j) 称作一个重要翻转对

你需要返回给定数组中的重要翻转对的数量。

示例 1:

输入: [1,3,2,3,1]
输出: 2

示例 2:

输入: [2,4,3,5,1]
输出: 3

注意:

  1. 给定数组的长度不会超过50000
  2. 输入数组中的所有数字都在32位整数的表示范围内。

这道题和逆序对那道题非常相似,逆序对是求前面的数大于后面的数的个数,而这道题是求前面的数大于后面数的两倍的个数

解法一:暴力枚举

暴力枚举就是固定一个数,枚举后面的数,以此类推,找到符合条件组合的个数,时间一定会超时,就不多说了,看下面的解法

解法二:分治

讲一个数组分为两部分,先求左边的翻转对的个数,再求右边翻转对的个数,再求一左一右的翻转对的个数

那么接下来,需要考虑怎么使用归并,能够解决此题

依旧是两种策略:

策略一:计算当前元素后面,有多少元素的2倍比我小(降序)

策略二:计算当前元素前面,有多少元素的一半比我大(升序)

与前面的计算方式不同的是,逆序对那道题,由于与递归排序的 if 语句中的判断条件是一样的,都是 nums[cur1] 与 nums[cur2] 比较,所以可以在递归的过程中,进行 ret++

而这道题是 nums[cur1] 与 2 * nums[cur2] 进行比较,不同的判断条件可能会导致遗漏,

所以在归并之前,先来判断这种情况有多少个翻转对,如果采用的是策略一,就让 ret += right - cur2 + 1,直到 cur1 <= mid 这个判断条件结束,下面再进行这一次的归并排序,以此类推


代码如下:

class Solution 
{
    int tmp[50010]; //辅助数组
public:
    int reversePairs(vector<int>& nums) 
    {
        return mergeSort(nums, 0, nums.size() - 1);
    }

    int mergeSort(vector<int>& nums, int left, int right)
    {
        if(left >= right) return 0;
        int ret = 0;
        //根据中间元素划分区间
        int mid = ((right - left) >> 1) + left;
        //计算左右两侧的翻转对
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1, right);
        //先计算翻转对的数量
        int cur1 =  left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid) //降序
        {
            while(cur2 <= right && nums[cur1] / 2.0 <= nums[cur2]) cur2++;
            if(cur2 > right) break;
            ret += right - cur2 + 1;
            cur1++;
        }
        //合并两个有序数组
        cur1 = left, cur2 = mid + 1;
        while(cur1 <= mid && cur2 <= right)
        {
            tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur2++] : nums[cur1++];
        }
        //处理没有处理完毕的数组
        while(cur1 <= mid) tmp[i++] = nums[cur1++];
        while(cur2 <= right) tmp[i++] = nums[cur2++];
        //恢复数组
        for(int i = left; i <= right; i++) nums[i] = tmp[i - left];
        return ret;
    }
};

分治中,关于归并的题目到此结束


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1844512.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Tailwindcss 提取组件

背景 随着项目的发展&#xff0c;您不可避免地会发现自己需要重复使用常用样式&#xff0c;以便在许多不同的地方重新创建相同的组件。这在小组件&#xff08;如按钮、表单元素、徽章等&#xff09;中最为明显。在我的项目中是图表标题样式如下&#xff1a; <div class&qu…

14-Kafka-Day03

第 5 章 Kafka 消费者 5.1 Kafka 消费方式 5.2 Kafka 消费者工作流程 5.2.1 消费者总体工作流程 一个消费者组中的多个消费者&#xff0c;可以看作一个整体&#xff0c;一个组内的多个消费者是不可能去消费同一个分区的数据的&#xff0c;要不然就消费重复了。 5.2.2 消费者…

SpringSecurity6从入门到实战之登录后操作

SpringSecurity6从入门到实战之登录后操作 上次已经了解了如何进行自定义登录页面,这次主要是详细讲解登录成功,登录之后的跳转以及包括退出登录等一系列操作.让我们来看看SpringSecurity需要如何进行配置 登录之后的跳转 定义 Spring Security 配置类 Configuration EnableW…

数据治理的七大核心技术 全面了解数据治理必读篇

在当今的数字化时代&#xff0c;数据已成为企业最宝贵的资产之一&#xff0c;其价值不仅体现在数据量的巨大&#xff0c;更在于数据的深度和宽度。随着大数据、云计算、物联网&#xff08;IoT&#xff09;和人工智能&#xff08;AI&#xff09;等技术的不断进步&#xff0c;企业…

25考研线代攻略,老师及习题册推荐!

其实很多经验贴对大家有一定的误导 网上很多人说李永乐讲的好&#xff0c;确实好&#xff0c;但是没有说听李永乐的线代需要一定的基础 于是很多人去听完李永乐&#xff0c;就懵逼了&#xff0c;这讲的很乱啊&#xff0c;听的一头雾水。 其实&#xff0c;李永乐的基础班授课…

天才简史——Diederik P. Kingma与他的Adam优化器

一、了解Diederik P. Kingma 发生日期&#xff1a;2024年6月18日 前几日&#xff0c;与实验室同门一同前往七食堂吃饭。饭间&#xff0c;一位做随机优化的同门说他看过一篇被引18w的文章。随后&#xff0c;我表示不信&#xff0c;说你不会数错了吧&#xff0c;能有1.8w次被引都…

智慧城市低空+AI视频智能监控:构建新时代安全防线

随着科技的飞速发展&#xff0c;智能监控技术已经广泛应用于各个领域&#xff0c;从城市治理到工业生产&#xff0c;从公共安全到环境监测&#xff0c;都发挥着越来越重要的作用。而在低空领域&#xff0c;AI视频智能监控方案的建设更是成为了一个热点话题。 一、低空AI视频智…

Java异常和文件

一、异常 1.定义 异常&#xff1a;异常就是代表程序出现的问题 体系结构&#xff1a; 最上层的是 Throwable 类&#xff0c;下面有两个子类&#xff1a; ① Error&#xff1a;代表系统级别的问题&#xff08;属于严重问题&#xff0c;比如&#xff1a;内存溢出&#xff09;。…

VScode基本使用

VScode下载安装&#xff1a; Visual Studio Code - Code Editing. Redefined MinGW的下载安装&#xff1a; MinGW-w64 - for 32 and 64 bit Windows - Browse Files at SourceForge.net x86是64位处理器架构&#xff0c;i686是32为处理器架构。 POSIX和Win32是两种不同的操…

java文件传输小工具 java17+springboot3+thymeleaf

背景 在和同事工作中经常需要传输文件&#xff0c;但是公网传输太慢&#xff0c;业务方不是计算机专业直接用命令行沟通麻烦。 本小工具通过页面可视化方便用户使用&#xff0c;端口9090&#xff0c;启动默认展示当前登陆本机用户的桌面。 代码开源&#xff1a; https://git…

SM9加密算法:安全、高效的国产密码技术

随着信息技术的飞速发展&#xff0c;网络安全问题日益凸显。加密算法作为保障信息安全的核心技术&#xff0c;受到了广泛关注。在我国&#xff0c;一种名为SM9的加密算法逐渐崭露头角&#xff0c;凭借其卓越的安全性能和高效计算能力&#xff0c;成为了新一代国产密码技术的代表…

NGINX_九 nginx_proxy代理

九 nginx_proxy代理 1.代理 1.1 代理原理 反向代理产生的背景&#xff1a; 在计算机世界里&#xff0c;由于单个服务器的处理客户端&#xff08;用户&#xff09;请求能力有一个极限&#xff0c;当用户的接入请求蜂拥而入时&#xff0c;会造成服务器忙不过来的局面&#xff0…

使用Jetpack Compose和DummyJSON加速你的Android开发

使用Jetpack Compose和DummyJSON加速你的Android开发 在现代Android开发中&#xff0c;Jetpack Compose提供了一种全新的UI构建方式&#xff0c;同时DummyJSON简化了开发过程中数据获取的复杂性。本文将详细介绍一个名为firefly-compose的Jetpack Compose模板应用程序&#xf…

电脑一键还原系统,小白也能轻松操作!

电脑一键还原系统是一项非常实用的功能&#xff0c;当电脑遇到无法解决的问题或需要恢复到出厂设置时&#xff0c;用户可以通过一键还原功能快速恢复系统到之前的状态。这项功能不仅可以节省时间&#xff0c;还能有效解决系统问题。本文将介绍三种电脑一键还原系统的方法&#…

【React】Lodash---groupBy() 分组

例子 _.groupBy([6.1, 4.2, 6.3], Math.floor); // > { 4: [4.2], 6: [6.1, 6.3] }// The _.property iteratee shorthand. _.groupBy([one, two, three], length); // > { 3: [one, two], 5: [three] }思路分析 来源 定义一个名为groupBy的方法&#xff0c;通过扩展Ar…

AI界的“视频滤镜”(Stable Diffusion进阶篇-TemporalKit视频风格转化),手把手教你制作原创AI视频

大家好&#xff0c;我是向阳 在之前的文章中我也分享过如何进行AI视频的制作&#xff0c;说是AI视频其实也就是通过Stable Diffusion进行视频重绘&#xff0c;也就是将一个视频一帧一帧重绘为自己想要的画面&#xff0c;然后再连贯起来成为视频。 这个东西其实比较耗费时间和…

智能猫砂盆是养猫必需品吗?三个好用品牌让你实现铲屎自动化!

随着现代社会的快节奏和压力增大&#xff0c;许多人开始因工作、旅行或其他紧急情况需要暂时离家&#xff0c;但这样的话&#xff0c;大家又要如何确保猫咪的猫砂盆在无人照料的情况下依旧保持清洁&#xff1f;尤其在炎热的季节&#xff0c;猫砂盆若长时间未得到清理&#xff0…

英伟达中国特供芯片降价背后:巨头与市场的较量

英伟达&#xff0c;这家曾经在人工智能芯片领域独领风骚的巨头&#xff0c;近期在中国市场遭遇了一些挑战。为了应对来自华为等中国本土企业的竞争&#xff0c;英伟达不得不采取降价策略&#xff0c;调整其专为中国市场打造的H20芯片价格&#xff0c;甚至低于华为的同类产品。这…

数据可视化实验五:seaborn绘制进阶图形

目录 一、绘制动态轨迹图 1.1 代码实现 1.2 绘制结果 二、使用seaborn绘制关系图 2.1 绘制散点图分析产品开发部已离职的员工的评分与平均工作时间 2.1.1 代码实现 2.1.2 绘制结果 ​编辑 2.2 基于波士顿房价数据&#xff0c;绘制房间数和房屋价格的折线图 2.2.1 代码…

人工智能产品经理,行业巨头争夺的稀缺人才

前言 在当今这个由数据驱动的时代&#xff0c;人工智能&#xff08;AI&#xff09;正迅速成为推动各行各业创新的核心力量。随着行业巨头纷纷布局人工智能领域&#xff0c;对于专业人才的需求也日益增长。特别是人工智能产品经理这一岗位&#xff0c;缺口高达6.8万&#xff0c…