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

news2024/11/27 18:30:57

一.基本概念与实现

归并排序(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. 本题在合并两个有序列表之前需要先统计翻转对的个数,具体做法就是暴力遍历两个数组,不能在合并的时候统计翻转对的个数

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

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

相关文章

基于java+springboot+vue实现的仓库管理系统(文末源码+lw+ppt)23-499

第1章 绪论 伴随着信息社会的飞速发展&#xff0c;仓库管理所面临的问题也一个接一个的出现&#xff0c;所以现在最该解决的问题就是信息的实时查询和访问需求的问题&#xff0c;以及如何利用快捷便利的方式让访问者在广大信息系统中进行查询、分享、储存和管理。这对我们的现…

QListWidget 缩略图IconMode示例

1、实现的效果如下&#xff1a; 2、实现代码 &#xff08;1&#xff09;头文件 #pragma once #include <QtWidgets/QMainWindow> #include "ui_QListViewDemo.h" enum ListDataType { ldtNone -1, ldtOne 0, ldtTwo 1, }; struct ListData…

系统化学习 H264视频编码(01)基础概念

说明&#xff1a;我们参考黄金圈学习法&#xff08;什么是黄金圈法则?->模型 黄金圈法则&#xff0c;本文使用&#xff1a;why-what&#xff09;来学习音H264视频编码。本系列文章侧重于理解视频编码的知识体系和实践方法&#xff0c;理论方面会更多地讲清楚 音视频中概念的…

读书笔记:终身成长

思维模式 两种思维模式对你意味着什么 相信自己的才能是一成不变的——也就是固定型的思维模式——会使你急于一遍遍地证明自己的能力。如果你只拥有一般水平的智力和品德&#xff0c;以及普通的个性——那么&#xff0c;你最好证明你自己能够在这些方面达到正常水平&#xf…

【Python】一文向您详细介绍 argparse中 action=‘store_true’ 的作用

【Python】一文向您详细介绍 argparse中 action‘store_true’ 的作用 下滑即可查看博客内容 &#x1f308; 欢迎莅临我的个人主页 &#x1f448;这里是我静心耕耘深度学习领域、真诚分享知识与智慧的小天地&#xff01;&#x1f387; &#x1f393; 博主简介&#xff1a;98…

自动控制:前馈控制

自动控制&#xff1a;前馈控制 前馈控制是一种在控制系统中通过预先计算和调整输入来应对已知扰动或变化的方法。相比于反馈控制&#xff0c;前馈控制能够更快速地响应系统的变化&#xff0c;因为它不依赖于系统输出的反馈信号。前馈控制的应用在工业过程中尤为广泛&#xff0…

DAMA学习笔记(四)-数据建模与设计

1.引言 数据建模是发现、分析和确定数据需求的过程&#xff0c;用一种称为数据模型的精确形式表示和传递这些数据需求。建模过程中要求组织发现并记录数据组合的方式。数据常见的模式: 关系模式、多维模式、面向对象模式、 事实模式、时间序列模式和NoSQL模式。按照描述详细程度…

染色の树-美团2023笔试(codefun2000)

题目链接 染色の树-美团2023笔试(codefun2000) 题目内容 输入描述 输出描述 输出一行一个整数表示根节点的值。 样例1 输入 3 1 1 2 2 2 输出 0 题解1 #include<bits/stdc.h> using namespace std;const int N 50005;int n, c[N];vector<int> edge[N];int dfs(…

【pytorch20】多分类问题

网络结构以及示例 该网络的输出不是一层或两层的&#xff0c;而是一个十层的代表有十分类 新建三个线性层&#xff0c;每个线性层都有w和b的tensor 首先输入维度是784&#xff0c;第一个维度是ch_out,第二个维度才是ch_in(由于后面要转置)&#xff0c;没有经过softmax函数和…

基于java+springboot+vue实现的校园外卖服务系统(文末源码+Lw)292

摘 要 传统信息的管理大部分依赖于管理人员的手工登记与管理&#xff0c;然而&#xff0c;随着近些年信息技术的迅猛发展&#xff0c;让许多比较老套的信息管理模式进行了更新迭代&#xff0c;外卖信息因为其管理内容繁杂&#xff0c;管理数量繁多导致手工进行处理不能满足广…

11.常见的Bean后置处理器

CommonAnnotationBeanPostProcessor (Resource PostConstructor PreDestroy) AutowiredAnnotationBeanPostProcessor (Autowired Value) GenericApplicationContext是一个干净的容器&#xff0c;它没有添加任何的PostProcessor处理器。 调用GenericApplicationContext.refre…

MSPM0G3507——编码器控制速度

绿色设置的为目标值100&#xff0c;红色为编码器实际数据 。 最后也是两者合在了一起&#xff0c;PID调试成功。 源码直接分享&#xff0c;用的是CCStheia&#xff0c;KEIL打不开。大家可以看一下源码的思路&#xff0c;PID部分几乎不用改 链接&#xff1a;https://pan.baid…

CSS【详解】长度单位 ( px,%,em,rem,vw,vh,vmin,vmax,ex,ch )

px 像素 pixel 的缩写&#xff0c;即电子屏幕上的1个点&#xff0c;以分辨率为 1024 * 768 的屏幕为例&#xff0c;即水平方向上有 1024 个点&#xff0c;垂直方向上有 768 个点&#xff0c;则 width:1024px 即表示元素的宽度撑满整个屏幕。 随屏幕分辨率不同&#xff0c;1px …

HCIE之IPV6和OSPFv6(十四)

IPV6 1、IPv6基础1.1 Ipv6地址静态配置、Eui 641.1.1 Ipv6地址静态配置1.1.2、Ipv6地址计算总结1.1.2.1、IEEE eui 64计算1.1.2.1.1、作用1.1.2.1.2、计算方法1.1.2.1.3、计算过程 1.1.2.2、被请求加入的组播组地址计算&#xff08;三层&#xff09;1.1.2.2.1、 作用1.1.2.2.2、…

数据结构与算法笔记:实战篇 - 剖析微服务接口鉴权限流背后的数据结构和算法

概述 微服务是最近几年才兴起的概念。简单点将&#xff0c;就是把复杂的大应用&#xff0c;解耦成几个小的应用 。这样做的好处有很多。比如&#xff0c;这样有利于团队组织架构的拆分&#xff0c;比较团队越大协作的难度越大&#xff1b;再比如&#xff0c;每个应用都可以独立…

BAT-致敬精简

什么是bat bat是windows的批处理程序&#xff0c;可以批量完成一些操作&#xff0c;方便快速。 往往我们可以出通过 winR键来打开指令窗口&#xff0c;这里输入的就是bat指令 这里就是bat界面 节约时间就是珍爱生命--你能想象以下2分钟的操作&#xff0c;bat只需要1秒钟 我…

深入理解JS逆向代理与环境监测

博客文章&#xff1a;深入理解JS逆向代理与环境监测 1. 引言 首先要明确JavaScript&#xff08;JS&#xff09;在真实网页浏览器环境和Node.js环境中有很多使用特性的区别。尤其是在环境监测和对象原型链的检测方面。本文将探讨如何使用JS的代理&#xff08;Proxy&#xff09…

分数的表示和运算方法fractions.Fraction()

【小白从小学Python、C、Java】 【考研初试复试毕业设计】 【Python基础AI数据分析】 分数的表示和运算方法 fractions.Fraction() 选择题 以下代码三次输出的结果分别是&#xff1f; from fractions import Fraction a Fraction(1, 4) print(【显示】a ,a) b Fraction(1, 2…

免费的鼠标连点器电脑版教程!官方正版!专业鼠标连点器用户分享教程!2024最新

电脑技术的不断发展&#xff0c;许多用户在日常工作和娱乐中&#xff0c;需要用到各种辅助工具来提升效率或简化操作&#xff0c;而电脑办公中&#xff0c;鼠标连点器作为一种能够模拟鼠标点击的软件&#xff0c;受到了广大用户的青睐。本文将为大家介绍一款官方正版的免费鼠标…

C++_STL---list

list的相关介绍 list是可以在常数范围内在任意位置进行插入和删除的序列式容器&#xff0c;并且该容器可以前后双向迭代。 list的底层是带头双向循环链表结构&#xff0c;链表中每个元素存储在互不相关的独立节点中&#xff0c;在节点中通过指针指向其前一个元素和后一个元素。…