首页 > 编程语言 >一文详解“分治—归并“在算法中的应用

一文详解“分治—归并“在算法中的应用

时间:2024-12-19 22:28:36浏览次数:11  
标签:归并 end nums int 分治 mid start 详解 merge

找往期文章包括但不限于本期文章中不懂的知识点:

个人主页:我要学编程(ಥ_ಥ)-CSDN博客

所属专栏: 优选算法专题

这里的归并与我们在数据结构中学习的归并排序是一样的,我们可以先来复习一下归并排序。用一道题来帮助我们回想起归并排序的细节。

目录

912.排序数组

LCR170.交易逆序对的总数

315.计算右侧小于当前元素的个数

493. 翻转对


912.排序数组

题目:

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

你必须在 不使用任何内置函数 的情况下解决问题,时间复杂度为 O(nlog(n)),并且空间复杂度尽可能小。

示例 1:

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

示例 2:

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

提示:

  • 1 <= nums.length <= 5 * 104
  • -5 * 104 <= nums[i] <= 5 * 104

思路:这里就是简单的使用归并排序来解决问题。

想了解归并排序更多细节的小伙伴,可以去看这篇文章:归并排序

代码实现:

class Solution {
    public int[] sortArray(int[] nums) {
        merge_sort(nums, 0, nums.length-1);
        return nums;
    }

    private void merge_sort(int[] nums, int start, int end) {
        // 递归的结束条件:只剩下一个元素
        if (start >= end) {
            return;
        }
        // 先分解左子树、再分解右子树
        int mid = (start+end) / 2;
        merge_sort(nums, start, mid);
        merge_sort(nums, mid+1, end);
        // 最后将两者合并
        merge(nums, start, mid, end);
    }

    // 合并两个有序数组的操作
    private void merge(int[] nums, int start, int mid, int end) {
        int s1 = start, e1 = mid, s2 = mid+1, e2 = end;
        // 借助辅助数组
        int[] array = new int[end-start+1];
        int k = 0;
        while (s1 <= e1 && s2 <= e2) {
            if (nums[s1] < nums[s2]) {
                array[k++] = nums[s1++];
            } else {
                array[k++] = nums[s2++];
            }
        }
        while (s1 <= e1) {
            array[k++] = nums[s1++];
        }
        while (s2 <= e2) {
            array[k++] = nums[s2++];
        }
        // 开始存放到原始数组中
        // 从start开始的,总共有K个元素
        for (int i = start; i < start+k; i++) { // 也可以直接写为end
            nums[i] = array[i-start];
        }
    }
}

LCR170.交易逆序对的总数

题目:

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

思路:这个题目的意思很简单,就是在数组随机找两个数,左边比右边大即可。暴力解法,直接固定左边或者右边的数,再去寻找另外一个数,最终把寻找到的组合数相加即可。

代码实现:

class Solution {
    // 暴力枚举
    public int reversePairs(int[] record) {
        int count = 0;
        for (int i = 0; i < record.length; i++) {
            for (int j = i+1; j < record.length; j++) {
                if (record[i] > record[j]) {
                    count++;
                }
            }
        }
        return count;
    }
}

很明显,这个方法的时间复杂度为 O(N^2),对于困难题肯定是不会就这么简单的通过的。因此接下来得想办法优化。

最初是在 整个数组中 找 左边 > 右边的组合,但是我们可以将数组划分为两块,左边 找 ,右边 找,最后再一左一右找,这也是符合要求的。 要注意的是,这里找到的组合都得是 前面的数 > 后面的数才行。但是上面的还是没有优化的地方,现在我们再来看一种方法:数组分为两块之后,我们在左边找完组合之后,继续对其进行排序,接着在右边找完组合之后,继续对右边进行排序,最终再找一左一右的组合。上述方法看似排序增加了时间消耗,但是会出现一个非常大的优化效果:排成升序之后,当 左边 数据 大于 右边的数据之后,我们可以下这样一个结论:左边当前位置以后的所有数据都是大于右边的数据的,这就一下子找出了很多数据。

上述过程完全对应着 归并排序。

注意:固定一方之后,只能在另一方找。

代码实现:

升序数组版:

class Solution {
    int[] temp; // 避免频繁创建数组,而造成的时间开销
    public int reversePairs(int[] record) {
        int len = record.length;
        temp = new int[len];
        // 在归并的过程中就统计
        return merge_sort(record, 0, len-1);
    }

    private int merge_sort(int[] nums, int start, int end) {
        if (start >= end) {
            return 0; // 不存在逆序对
        }
        // 递归
        int mid = (start + end) / 2;
        int ret = 0;
        ret += merge_sort(nums, start, mid);
        ret += merge_sort(nums, mid+1, end);
        // 合并
        // 统计
        int left = start, right = mid+1, i = 0;
        while (left <= mid && right <= end) {
            if (nums[left] <= nums[right]) {
                temp[i++] = nums[left++];
            } else {
                ret += (mid-left+1);
                temp[i++] = nums[right++];
            }
        }
        while (left <= mid) {
            temp[i++] = nums[left++];
        }
        while (right <= end) {
            temp[i++] = nums[right++];
        }
        // 排序
        for (int j = start; j <= end; j++) {
            nums[j] = temp[j-start];
        }
        return ret;
    }
}

注意:我们这里一定是先统计完当前的数组,再去对当前数组进行排序的。例如,左边数组统计完,右边数组统计完,接着再统计一左一右的情况(这里的数据相对位置还是没有发生变化的),最后再对两者进行排序、

降序数组版:

class Solution {
    int[] temp; // 避免频繁创建数组,而造成时间开销
    public int reversePairs(int[] record) {
        int len = record.length;
        temp = new int[len];
        return merge_sort(record, 0, len-1);
    }

    private int merge_sort(int[] nums, int start, int end) {
        if (start >= end) {
            return 0; // 不存在逆序对
        }
        int ret = 0;
        // 递归
        int mid = (start+end) / 2;
        ret += merge_sort(nums, start, mid);
        ret += merge_sort(nums, mid+1, end);
        // 合并
        // 统计
        int left = start, right = mid+1, i = 0;
        while (left <= mid && right <= end) {
            if (nums[left] <= nums[right]) {
                temp[i++] = nums[right++];
            } else {
                ret += (end-right+1);
                temp[i++] = nums[left++];
            }
        }
        while (left <= mid) {
            temp[i++] = nums[left++];
        }
        while (right <= end) {
            temp[i++] = nums[right++];
        }
        // 排序
        for (int j = start; j <= end; j++) {
            nums[j] = temp[j-start];
        }
        return ret;
    }
}

315.计算右侧小于当前元素的个数

题目:

给你一个整数数组 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]

提示:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104

思路:这一题与上一题是差不多的,总体上的思路是一致的,只是这里需要返回一个新数组存储的是原数组对应位置大于后面元素的个数即可。因此我们需要采取策略二:固定一个数,找到比当前元素小的数。但由于这里是需要排序的,就会导致元素的下标发生变化,这就需要我们去存储这些下标对应的元素,不过这里不能用哈希的方式,因为这里出现了重复的元素。有一种非常简单的方法:绑定下标与之对应的元素。如果元素发生变化,那么与之对应的下标也随之发生变化即可。因此我们可以申请一个下标数组,到时候如果元素之间交换,下标也要去进行交换。

代码实现:

class Solution {
    // 申请 下标数组、最终结果数组、辅助下标数组、辅助数据数组
    int[] index;
    int[] ret;
    int[] temp_index;
    int[] temp_nums;

    public List<Integer> countSmaller(int[] nums) {
        int n = nums.length;
        // 初始化数组
        index = new int[n];
        ret = new int[n];
        temp_index = new int[n];
        temp_nums = new int[n];
        for (int i = 0; i < n; i++) {
            // 逻辑上的绑定
            index[i] = i;
        }

        merge(nums, 0, n-1);

        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            list.add(ret[i]);
        }
        return list;
    }

    private void merge(int[] nums, int start, int end) {
        // 1、防止越界
        if (start >= end) {
            return;
        }
        // 2、处理左侧数据、处理右侧数据
        int mid = (start+end) / 2;
        merge(nums, start, mid);
        merge(nums, mid+1, end);
        // 3、处理一左一右的情况
        int left = start, right = mid+1, i = 0;
        while (left <= mid && right <= end) { // 进行降序排序
            if (nums[left] <= nums[right]) { // 不符合要求,直接排序即可
                // 既要把基本的数据排序,也要对绑定的下标排序
                temp_nums[i] = nums[right];
                temp_index[i++] = index[right++];
            } else {
                // 统计对应下标的数据(这里一定得是原始下标)
                ret[index[left]] += (end-right+1);
                // 继续进行排序
                temp_nums[i] = nums[left];
                temp_index[i++] = index[left++];
            }
        }
        
        while (left <= mid) {
            temp_nums[i] = nums[left];
            temp_index[i++] = index[left++];
        }
        
        while (right <= end) {
            temp_nums[i] = nums[right];
            temp_index[i++] = index[right++];
        }
        // 4、处理剩下的排序
        for (int j = start; j <= end; j++) {
            // 基本数据 + 下标
            nums[j] = temp_nums[j-start];
            index[j] = temp_index[j-start];
        }
    }
}

注意:策略二,我们采取的是降序排序。

493. 翻转对

题目:

给定一个数组 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位整数的表示范围内。

思路:本题与前面两题差不多的思路,但稍微有点不同,这里我们不再是统计 nums[i] > nums[j] 了,而是统计 nums[i] > 2*nums[j] ,这里就需要注意了,不能再将 统计的代码 与 归并排序的代码混在一起了,前面我们之所以将两者混在一起是因为统计的判断逻辑与归并排序的判断逻辑相同,因此这里得先去统计,然后再去排序。

代码实现: 

数组升序版:

class Solution {
    int[] temp; // 申请辅助数组
    public int reversePairs(int[] nums) {
        int n = nums.length;
        temp = new int[n];
        return merge(nums, 0, n-1);
    }

    private int merge(int[] nums, int start, int end) {
        // 处理不符合要求的
        if (start >= end) {
            return 0;
        }
        // 处理左边、右边
        int ret = 0;
        int mid = (start+end) / 2;
        ret += merge(nums, start, mid);
        ret += merge(nums, mid+1, end);
        // 处理一左一右的情况
        int left = start, right = mid+1, i = 0;
        while (left <= mid && right <= end) { // 只有两个区间都有值时,才去计算
            // 这里得去处理数据溢出的问题
            if ((long)(nums[left]) <= 2*(long)nums[right]) { // 不符合要求的
                left++;
            } else { // 符合要求的
                ret += mid-left+1;
                right++;
            }
        }
        // 进行归并排序
        left = start;
        right = mid+1;
        while (left <= mid && right <= end) { // 采取升序排序
            if (nums[left] <= nums[right]) {
                temp[i++] = nums[left++];
            } else {
                temp[i++] = nums[right++];
            }
        }
        
        while (left <= mid) {
            temp[i++] = nums[left++];
        }

        while (right <= end) {
            temp[i++] = nums[right++];
        }
        // 处理最后的排序问题
        for (int j = start; j <= end; j++) {
            nums[j] = temp[j-start];
        }
        return ret;
    }
}

数组降序版:

class Solution {
    int[] temp; // 申请辅助数组
    public int reversePairs(int[] nums) {
        int n = nums.length;
        temp = new int[n];
        return merge(nums, 0, n-1);
    }

    private int merge(int[] nums, int start, int end) {
        // 处理不符合要求的
        if (start >= end) {
            return 0;
        }
        // 处理左边、右边
        int ret = 0;
        int mid = (start+end) / 2;
        ret += merge(nums, start, mid);
        ret += merge(nums, mid+1, end);
        // 处理一左一右的情况
        int left = start, right = mid+1, i = 0;
        while (left <= mid && right <= end) { // 只有两个区间都有值时,才去计算
            // 这里得去处理数据溢出的问题
            if ((long)(nums[left]) <= 2*(long)nums[right]) { // 不符合要求的
                right++;
            } else { // 符合要求的
                ret += end-right+1;
                left++;
            }
        }
        // 进行归并排序
        left = start;
        right = mid+1;
        while (left <= mid && right <= end) { // 采取降序排序
            if (nums[left] <= nums[right]) {
                temp[i++] = nums[right++];
            } else {
                temp[i++] = nums[left++];
            }
        }
        
        while (left <= mid) {
            temp[i++] = nums[left++];
        }

        while (right <= end) {
            temp[i++] = nums[right++];
        }
        // 处理最后的排序问题
        for (int j = start; j <= end; j++) {
            nums[j] = temp[j-start];
        }
        return ret;
    }
}

好啦!本期 一文详解“分治—归并“在算法中的应用 的学习之旅 就到此结束啦!

标签:归并,end,nums,int,分治,mid,start,详解,merge
From: https://blog.csdn.net/2301_80854132/article/details/144013226

相关文章

  • HelloWorld详解
    HelloWorld随便新建一个文件夹,存放代码新建一个java文件文件名后缀名为.javaHello.java[注意点]需要手动打开文件扩展名编写代码publicclassHello{ publicstaticvoidmain(String[]args){ System.out.print("Hello,World!"); }}编译javacjava文件......
  • 快速排序与归并排序
    算法竞赛中,往往更注重时间复杂度上的优化,因此在这里介绍两种快速的排序算法。无论是快速排序还是归并排序,他们的思想都是分治归并排序我们给一组数据:95271243111 最终期望输出结果:12345791112现在开始酣畅淋漓的画图分析:当然现在从理论分析到实操还是......
  • 排序算法(冒泡,快排,归并)
    冒泡排序:(从小到大)1.比较相邻元素,若第一个元素比第二个大,就交换两个2.对相邻元素做同样步骤,从第一对元素到最后一对元素,直到最后的元素最大3.对所有元素重复以上步骤,除了最后一个;重复步骤1-3,直到排序完成publicstaticvoidsort(intw[]){for(inti=0;i<w.length-1;......
  • Java设计模式 —— 【结构型模式】桥接模式详解
    前言现在有一个需求,需要创建不同的图形,并且每个图形都有可能会有不同的颜色。首先我们看看用继承来实现:我们可以发现有很多的类,假如我们再增加一个形状或再增加一种颜色,就需要创建更多的类。试想,在一个有多种可能会变化的维度的系统中,用继承方式会造成类爆炸,扩展起来不......
  • 【01】优雅草央千澈详解关于APP签名以及分发-上架完整流程-如何将安卓APP-apk包和IOS
    【01】优雅草央千澈详解关于APP签名以及分发-上架完整流程-如何将安卓APP-apk包和IOS苹果app-ipa包上架至应用商店-安卓以华为|小米|vivo|oppo|应用宝为例-苹果上架以appstore为例合计三篇背景介绍2024年12月13日优雅草APP分发平台youyacao.cn建立,提供服务(优雅草2019年就曾建......
  • Nmap脚本参数详解
    免责声明:使用本教程或工具,用户必须遵守所有适用的法律和法规,并且用户应自行承担所有风险和责任。文章目录一、按脚本分类1.检查身份验证机制2.探测广播行为3.登录爆破4.默认脚本运行5.网络资产发现6.Dos漏洞检测7.漏洞利用8.检测威胁主机9.模糊测试10.侵入......
  • 网络编程一>HTTP协议详解,<一文搞懂HTTP协议,抓包工具使用,HTTP协议报头>
    目录:  一.获取HTTP协议: 二.HTTP基本格式及格式内容: 三.HTTP请求"报头"详情(header):  一.获取HTTP协议:一.HTTP是什么HTTP(全称为"超文本传输协议")是⼀种应用非常广泛的应用层协议. 当我们在浏览器中输入⼀个"网址",此时浏览器就会给对应的服务......
  • 新手必看!项目管理十大领域详解(附工具推荐)
    项目管理技能:未来职场的“硬通货”从数据上看,项目管理不是未来的“可能性”,而是职业发展的“确定性”:到2027年,全球市场对项目管理专业人员的需求可能将达到8770万+,这一技能不再是某些特定行业的“小众需求”,而是各行各业的“必备项”。企业需要什么样的人才?81%的公司在疯狂打磨......
  • Linux 定时任务操作详解及python简单的任务管理器
    Linux定时任务操作在Linux中,定时任务操作主要通过cron工具来实现。cron是一个基于时间的作业调度器,允许用户在指定的时间或周期内执行预定的任务。1.查看当前用户的定时任务使用crontab命令查看当前用户的定时任务:crontab-l2.编辑定时任务使用crontab-e......
  • 超绝!基站/Wi-Fi/GPS定位技术详解与应用示例
    今天特别分享定位相关示例,欢迎大家一起来探讨。一、基站/Wi-Fi/GPS定位概述1.1基站定位原理基站定位也就是“LBS定位”,全称是LocationBasedService,它包括两层含义:首先是确定移动设备或用户所在的地理位置;其次是提供与位置相关的各类信息服务。意指与定位相关的各类服务系......