算法系列--分治排序|归并排序|逆序对的求解

一.基本概念与实现

归并排序(mergeSort)也是基于分治思想的一种排序方式,思路如下:

  1. 分解:根据中间下标mid将数组分解为两部分
  2. 解决:不断执行上述分解过程,当分解到只有一个元素时,停止分解,此时就是有序的
  3. 合并:合并两个有序的子区间,所有子区间合并的结果就是原问题的解

归并排序和快速排序的区别在于排序的时机不同,归并排序是等到分解完毕之后在进行排序,快速排序是先进行分解,再排序;更形象的说,归并排序更像二叉树的后序遍历,遍历完左右子树再打印根节点;快速排序反之
在这里插入图片描述

代码:

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

    private void mergeSort(int[] nums, int start, int end) {
        if(start >= end) return;// 递归出口
        int mid = (start + end) >> 1;

		// 分解左右子树
        mergeSort(nums, start, mid);
        mergeSort(nums, mid + 1, end);
		
		// 合并两个有序序列
        merge(nums, start, mid, end);
    }

    private void merge(int[] nums, int l, int mid, int r) {
        // 合并两个有序列表
        int i = 0, i1 = l, i2 = mid + 1;
        while(i1 <= mid && i2 <= r)
            tmp[i++] = nums[i1] < nums[i2] ? nums[i1++] : nums[i2++];
        
        while(i1 <= mid) tmp[i++] = nums[i1++];
        while(i2 <= r) tmp[i++] = nums[i2++];
		
		// 重新赋值
        for(int j = l; j <= r; j++) nums[j] = tmp[j - l];
    }
}
  • 将tmp设置为全局减少每次合并两个有序列表都要new所消耗的时间

2.利用归并排序求解逆序对

逆序对
链接:https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/description/
在这里插入图片描述
分析

  • 最容易想到的思路就是暴力解法,每遍历到一个数字就统计后面有多少个小于当前数字的数字,只要小于就满足逆序对的条件,时间复杂度为O(N^2),显而易见这种做法的时间复杂度过高,无法通过所有样例

  • 可以这么想,每遍历到一个数字,我就想"如果我知道我前面有多少个比我大的数字该多好",就不需要再去一个一个遍历了,或者是每遍历到一个数字,“如果我知道后面有多少个比我小的数字该多好”

  • 但是实际上不容易解决,不容易统计,比如9,7,8这样的一个序列,你无法通过记录最值的方式统计有多少个大的数字,这是行不通的,好像唯一的解法就是从开头一直遍历到当前数字?可这就是暴力解法啊,如果能对前面的区间进行排序,得到大于当前数字的所有数字中的最小值,就能根据下标快速得到有多少个比我大的数字.那难道每遍历到一个数字就对前面的区间排一下序再找最小值吗?时间复杂度好像更高了,且不稳定

  • 但是这个想法是好的,在遍历的过程中如果前面的元素是有序的,那么就能降低时间复杂度,关于逆序对的求解,排序是一个很好的思路

  • 排序一般都是从小到大排序,无论哪种排序方式,对于一个乱序的数组,如果想让他变成有序的,就必须要进行元素的移动,就会将较小的数字放到较大的数字之前,可见排序就是消除逆序对的过程,以下是几种基于排序的求解逆序对的方法

01.冒泡排序

  • 对于冒泡排序,每次元素交换就可以看做一次消除逆序对;使用全局变量ret记录元素交换的次数
  • 冒泡排序的本质是"每次移动,都通过元素比较和元素交换让当前区间的最大值移动到区间末尾"

代码:

class Solution {
    // 冒泡排序法
    int ret;
    public int reversePairs(int[] nums) {
        bubbleSort(nums);
        return ret;
    }

    private void bubbleSort(int[] nums) {
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            boolean flg = false;
            for (int j = 0; j < n - 1 - i; j++) {
                if (nums[j] > nums[j + 1]) {
                    swap(nums, j, j + 1);
                    flg = true;
                }
            }

            if (!flg)
                return;
        }
    }

    private void swap(int[] nums, int i, int j) {
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
        ++ret;
    }
}
  • 时间复杂度:O(N^2)

02.插入排序

  • 对于插入排序,每次元素的移动都可以看做一次消除逆序对,使用一个全局变量ret记录元素移动的次数即可,时间比冒泡排序略快
  • 插入排序就像是往你的已有的有序的扑克牌中插入一个新牌,每次都是从后往前进行比较,进行插入

代码:

class Solution {
    // 插入排序法
    int ret;
    public int reversePairs(int[] nums) {
        for(int i = 1; i < nums.length; i++) {
            int tmp = nums[i], j = i - 1;
            while(j >= 0) {
                if(nums[j] > tmp) {
                    nums[j + 1] = nums[j];
                    ++ret;
                }
                else break;
                --j;
            }

            nums[j + 1] = tmp;
        }
    
        return ret;
    }
}
  • 时间复杂度:O(N^2)

03.归并排序(最快的解法)

代码:

class Solution {
    int[] tmp;// 用于合并两个有序数组的临时数组
    int ret;// 记录递归过程中的逆序对的个数
    public int reversePairs(int[] nums) {
        tmp = new int[nums.length];
        mergeSort(nums, 0, nums.length - 1);// 归并排序
        return ret;
    }

    private void mergeSort(int[] nums, int start, int end) {
        if(start >= end) return;
        int mid = (start + end) >> 1;
        mergeSort(nums, start, mid);
        mergeSort(nums, mid + 1, end);
        merge(nums, start, mid, end);
    }

    private void merge(int[] nums, int l, int mid, int r) {
        // 在合并的过程中统计逆序对的个数
        // 关键步骤  合并两个有序数组
        // 在合并的过程中统计逆序对的个数!!!
        // 此处采用 的逻辑是:固定cur2,找cur1中比当前cur2大的数
        // 在排序的过程中应该使用升序排序
        int i = 0, i1 = l, i2 = mid + 1;
        while(i1 <= mid && i2 <= r) {
            if(nums[i1] > nums[i2]) {// 如果大  则i1后面的数字都比当前数字大  都能构成逆序对
                ret += mid - i1 + 1;
                tmp[i++] = nums[i2++];
            }else {
                tmp[i++] = nums[i1++];
            }
        }

        while(i1 <= mid) tmp[i++] = nums[i1++];
        while(i2 <= r) tmp[i++] = nums[i2++];

        for(int j = l; j <= r; j++) nums[j] = tmp[j - l];
    }
}
  • 时间复杂度:O(N*logN)

图解:
在这里插入图片描述
在这里插入图片描述


3.计算右侧小于当前数的个数

链接:https://leetcode.cn/problems/count-of-smaller-numbers-after-self/description/
分析

  • 是上一题逆序对的拓展版本
  • 利用上面快速求解逆序对的思想,能够快速得到小于当前数字的数字的个数,需要注意的是,在归并排序的过程中原数组的元素会发生移动,但是只有归并完整个数组才能得到最终的结果,这个过程中我们要将数组元素和数组下标进行绑定,可以考虑使用index数组
  • 当元素发生移动时,下标也跟着移动;

代码:

class Solution {
    int[] tmpNum;
    int[] tmpIndex;
    int[] index;
    int[] cnt;

    public List<Integer> countSmaller(int[] nums) {
        int n = nums.length;
        tmpNum = new int[n];
        tmpIndex = new int[n];
        index = new int[n];
        cnt = new int[n];

        for(int i = 0; i < n; i++) index[i] = i;// 绑定索引
        mergeSort(nums, 0, n - 1);
        List<Integer> ret = new ArrayList<>();
        for(int x : cnt) ret.add(x);
        return ret;
    }

    private void mergeSort(int[] nums, int start, int end) {
        if(start >= end) return;
        int mid = (start + end) >> 1;
        mergeSort(nums, start, mid);
        mergeSort(nums, mid + 1, end);
        merge(nums, start, mid, end);
    }

    private void merge(int[] nums, int l, int mid, int r) {
        // 合并两个有序列表(降序排序)
        // 可以快速得到有多少个数字小于当前数字
        int i = 0, i1 = l, i2 = mid + 1;
        while(i1 <= mid && i2 <= r) {
            if(nums[i1] > nums[i2]) {
                cnt[index[i1]] += r - i2 + 1;
                tmpIndex[i] = index[i1];
                tmpNum[i++] = nums[i1++];
            }else  {
                tmpIndex[i] = index[i2];
                tmpNum[i++] = nums[i2++];
            }
        }

		// 处理未解决的元素
        while(i1 <= mid) {
            tmpIndex[i] = index[i1];
            tmpNum[i++] = nums[i1++];
        }

        while(i2 <= r) {
            tmpIndex[i] = index[i2];
            tmpNum[i++] = nums[i2++];
        }

        // 重新赋值  原数组和下标数组都要更改
        for(int j = l; j <= r; j++) {
            nums[j] = tmpNum[j - l];
            index[j] = tmpIndex[j - l];
        }
    }
}

4.反转对的个数

链接:

代码:

class Solution {
    int[] tmp;
    int ret;
    public int reversePairs(int[] nums) {
        int n = nums.length;
        tmp = new int[n];
        mergeSort(nums, 0, n - 1);
        return ret;
    }

    private void mergeSort(int[] nums, int start, int end) {
        if(start >= end) return;
        int mid = (start + end) >> 1;
        mergeSort(nums, start, mid);
        mergeSort(nums, mid + 1, end);
        merge(nums, start, mid, end);
    }
    
    private void merge(int[] nums, int l, int mid, int r) {
        // 先统计当前有多少个翻转对
        int i = 0, i1 = l, i2 = mid + 1;
        while(i1 <= mid) {
            while(i2 <= r && nums[i2] >= nums[i1] / 2.0) i2++;// 注意此处的除法需要存在小数位
            if(i2 > r) break;
            ret += r - i2 + 1;
            i1++;
        }

        i1 = l; i2 = mid + 1;
        while(i1 <= mid && i2 <= r) {
            if(nums[i1] > nums[i2]) tmp[i++] = nums[i1++];
            else tmp[i++] = nums[i2++];
        }

        while(i1 <= mid) tmp[i++] = nums[i1++];
        while(i2 <= r) tmp[i++] = nums[i2++];

        for(int j = l; j <= r; j++) nums[j] = tmp[j - l];
    }
}

注意:

  1. 使用除法进行判断是为了防止越界
  2. /2和/2.0的结果是不同的
  3. 本题在合并两个有序列表之前需要先统计翻转对的个数,具体做法就是暴力遍历两个数组,不能在合并的时候统计翻转对的个数

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ESP32 蓝牙网关实践:BLE 设备数据采集与 MQTT 云平台发布(附代码示例)

摘要: 本文详细介绍了如何使用 ESP32 构建强大的蓝牙网关&#xff0c;实现蓝牙设备与 Wi-Fi/互联网之间的无缝连接和数据桥接。文章涵盖了连接和桥接功能、数据处理和分析能力&#xff0c;并提供了详细的代码示例和 Mermaid 生成的图表&#xff0c;助您轻松构建自己的蓝牙网关解…

SCI一区TOP|准随机分形搜索算法(QRFS)原理及实现【免费获取Matlab代码】

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4.参考文献5.代码获取 1.背景 2024年&#xff0c;LA Beltran受到分形几何、低差异序列启发&#xff0c;提出了准随机分形搜索算法&#xff08;Quasi-random Fractal Search, QRFS&#xff09;。 2.算法原理 2.1算法思…

【Python】搭建属于自己 AI 机器人

目录 前言 1 准备工作 1.1 环境搭建 1.2 获取 API KEY 2 写代码 2.1 引用库 2.2 创建用户 2.3 创建对话 2.4 输出内容 2.5 调试 2.6 全部代码 2.7 简短的总结 3 优化代码 3.1 规范代码 3.1.1 引用库 3.1.2 创建提示词 3.1.3 创建模型 3.1.4 规范输出&#xf…

SQL面试题-留存率计算

表定义&#xff1a; create table if not exists liuliang_detail (user_id string comment ,record_time string comment yyyymmdd hh:mi:ss ) comment 流量明细表 ; 方法一&#xff1a; 计算的是整段时间范围内&#xff0c;每一天为基准的所有的留存1、2、7天的用户数。 …

cs231n作业2 双层神经网络

双层神经网络 我们选用ReLU函数和softmax函数&#xff1a; 步骤&#xff1a; 1、LOSS损失函数&#xff08;前向传播&#xff09;与梯度&#xff08;后向传播&#xff09;计算 Forward: 计算score&#xff0c;再根据score计算loss Backward&#xff1a;分别对W2、b2、W1、b1求…

使用Charles mock服务端响应数据

背景 服务端未提供接口/服务端接口返回结果有逻辑限制&#xff08;次数限制&#xff09;&#xff0c;不能通过原始接口返回多次模拟预期的返回结果&#xff0c;例如边界值情况 客户端受到接口响应数据的限制&#xff0c;无法继续开发或测试&#xff0c;会极大影响开发测试效率…

Unity入门之重要组件和API(3) : Transform

前言 Transform类主要处理游戏对象(GameObject)的位移、旋转、缩放、父子关系和坐标转换。 1.位置和位移 1.1必备知识点&#xff1a;Vector3 Vector3 主要用来表示三维坐标系中的一个点或者一个向量。 【声明】 Vector3 v1 new Vector3(); Vector3 v2 new Vector3(10, 10…

谷粒商城----通过缓存和分布式锁获取数据。

高并发下缓存失效的问题 高并发下缓存失效的问题--缓存穿透 指查询一个一定不存在的数据&#xff0c;由于缓存是不命中&#xff0c;将去查询数据库&#xff0c;但是数据库也无此记录&#xff0c;我们没有将这次查询的不写入缓存&#xff0c;这将导致这个不存在的数据每次请求…

详解「一本通 5.1 练习 1」括号配对(区间DP经典题)

一.题目 二.思路 题目的大意是说:给你一个只由[ ] ( )构成的字符串&#xff0c;请问需要增加多少个字符才能使其变为一个合法的括号序列。 因为添加若干字符使其达到匹配的目的等价于将不匹配的字符去除使得字符串达到匹配的目的 所以这题只需计算出已匹配完成的括号数,再…

深度学习与CV入门

文章目录 前言历史 前言 历史 tensorflow可以安装Tensorboard第三方库用于展示效果 TensorFlow工作流程&#xff1a;p6-4:20 使用tf.data加载数据。使用tf.data实例化读取训练数据和测试数据模型的建立与调试:使用动态图模式Eager Execution和著名的神经网络高层API框架Ker…

mongoDB教程(五):命名规范

还是大剑师兰特&#xff1a;曾是美国某知名大学计算机专业研究生&#xff0c;现为航空航海领域高级前端工程师&#xff1b;CSDN知名博主&#xff0c;GIS领域优质创作者&#xff0c;深耕openlayers、leaflet、mapbox、cesium&#xff0c;canvas&#xff0c;webgl&#xff0c;ech…

拍桌子、甩脸子、抡棒子没用,带出一流战斗力团队用好3招就够了

拍桌子、甩脸子、抡棒子没用&#xff0c;带出一流战斗力团队用好3招就够了 第一招&#xff1a;及时激励 在现实中&#xff0c;绝大部分管理者管理手段缺乏&#xff0c;只知道用钱进行激励。 而真正的高手不仅会满足员工物质上的需求&#xff0c;更注重员工心理上的满足。 他…

cs231n作业1——Softmax

参考文章&#xff1a;cs231n assignment1——softmax Softmax softmax其实和SVM差别不大&#xff0c;两者损失函数不同&#xff0c;softmax就是把各个类的得分转化成了概率。 损失函数&#xff1a; def softmax_loss_naive(W, X, y, reg):loss 0.0dW np.zeros_like(W)num_…

知识社区在线提问小程序模板源码

蓝色的知识问答&#xff0c;问答交流&#xff0c;知识社区&#xff0c;在线提问手机app小程序网页模板。包含&#xff1a;社区主页、提问、我的、绑定手机&#xff0c;实名认证等。 知识社区在线提问小程序模板源码

**kwargs 字典解包传参的方式

字典解包传参 在Python中&#xff0c;****kwargs**是一种通过字典解包 (dictionary unpacking) 的方式进行参数传递的方式。它将一个字典的键值对解包并传递给函数的命名参数。 示例代码 kwargs实参: {name: "jordan", age: 18, score: [80, 85, 85]} get_info形…

U盘非安全退出后的格式化危机与高效恢复策略

在数字化时代&#xff0c;U盘作为数据存储与传输的重要工具&#xff0c;其数据安全备受关注。然而&#xff0c;一个常见的操作失误——U盘没有安全退出便直接拔出&#xff0c;随后再插入时却遭遇“需要格式化”的提示&#xff0c;这不仅让用户措手不及&#xff0c;更可能意味着…

windows内置的hyper-v虚拟机的屏幕分辨率很低,怎么办?

# windows内置的hyper-v虚拟机的屏幕分辨率很低&#xff0c;怎么办&#xff1f; 只能这么大了&#xff0c;全屏也只是把字体拉伸而已。 不得不说&#xff0c;这个hyper-v做的很烂。 直接复制粘贴也做不到。 但有一个办法可以破解。 远程桌面。 我们可以在外面的windows系统&…

科普文:构建可扩展的微服务架构设计方案

前言 微服务架构是一种新兴的软件架构风格&#xff0c;它将单个应用程序拆分成多个小的服务&#xff0c;每个服务都运行在自己的进程中&#xff0c;这些服务通过网络进行通信。这种架构的优势在于它可以提高应用程序的可扩展性、可维护性和可靠性。 在传统的应用程序架构中&…

minist数据集分类模型的训练

minist数据集训练 训练方法&#xff1a;利用pytorch来实现minist数据集的分类模型训练 训练模型如下图所示 模型代码&#xff1a; import torch from torch import nn from torch.nn import Flattenclass Net(nn.Module):def __init__(self):super().__init__()self.module …

grid布局下的展开/收缩过渡效果【vue/已验证可正常运行】

代码来自GPT4o&#xff1a;国内官方直连GPT4o <template><div class"container"><button class"butns" click"toggleShowMore">{{ showAll ? 收回 : 显示更多 }}</button><transition-group name"slide-fade&…