剑指 Offer 51:数组中的逆序对 (Java分治思想)

简介: 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

一、题目描述



在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。


示例 1:

输入: [7,5,6,4]

输出: 5

 

限制:

0 <= 数组长度 <= 50000


二、思路讲解



首先一眼看上去,二重循环暴力十分省事,然而时间复杂度为平方阶,超时。

public class Solution {
    public int reversePairs(int[] nums) {
        int count = 0
        for (int i = 0; i <nums.length-1; i++) {
            for (int j = i + 1; j < len; j++) {
                if (nums[i] > nums[j]) {
                    count++;
                }
            }
        }
        return count;
    }
}


那么我们就思考更好的算法:

     

求逆序对类型的题目里,经常使用归并排序,因为在高级排序算法(归并排序、快速排序)里,能够明显看到阶段性排序效果的算法就是归并排序。


举个例子,例如在归并排序的最后一步:合并的过程中,有前后两部分(这两部分分别有序的):


       5        6        7        8    I    1        2        3        4


       ↑                                       ↑


       i                                         j


根据合并的思路,j指针所指小于i指针所指,会将4首先放回拷贝数组中。因为左半边数组也是有序的,所以1小于左半边的所有数字,因此逆序对个数可以直接加4。详解见力扣官方视频,讲得很好:数组中的逆序对


三、Java代码实现



其实只用在归并排序中加一行代码就行了。


代码中有几个细节问题:


int mid = left +(right - left) / 2; 避免left+right超过int范围;

     

归并排序中,合并时,如果nums[x]<=nums[y]的等号不取,排序将不稳定

class Solution {
    int count = 0;
    public int reversePairs(int[] nums) {
        this.count = 0;
        mergeSort(nums, 0, nums.length-1, new int[nums.length]);
        return count;
    }
    //传一个用于拷贝的temp数组,比每次都创建新数组的效率要更高
    void mergeSort(int []nums, int i, int j, int []temp) {
        if(i >= j) {
            return;
        }
        int k = i + (j-i)/2;    //取中间值,这样的写法避免i+j超过整型范围
        mergeSort(nums, i, k, temp);
        mergeSort(nums, k+1, j, temp);
        merge(nums, i, k, j, temp);
    }
    void merge(int []nums, int i, int k, int j, int []temp) {
        int x = i;
        int y = k+1;
        int index = 0;
        while(x<=k && y<=j) {
            while(x<=k && nums[x]<=nums[y]) {   //如果这里nums[x]<=nums[y]不写等号,归并排序将不稳定
                temp[index++] = nums[x++];
            }
            while(y<=j && nums[y]<nums[x]) { //注意这里不能写等号,因为逆序对是不能相等的
                count += (k-x+1);   //比归并排序多的一行
                temp[index++] = nums[y++];
            }
        }
        while(x<=k) {
            temp[index++] = nums[x++];
        }
        while(y<=j) {
            temp[index++] = nums[y++];
        }
        for(int m=i; m<=j; m++) {
            nums[m] = temp[m-i];
        }
    }
}


四、时空复杂度分析



时间复杂度:        O(NlogN)        归并排序的时间复杂度


空间复杂度:        O(N)                归并排序的空间复杂度,因为需要一个拷贝数组


相关文章
Java 数组学习笔记
本文整理Java数组常用操作:遍历、求和、查找、最值及二维数组行求和等典型练习,涵盖静态初始化、元素翻倍、去极值求平均等实例,帮助掌握数组基础与应用。
|
存储 缓存 Java
Java数组全解析:一维、多维与内存模型
本文深入解析Java数组的内存布局与操作技巧,涵盖一维及多维数组的声明、初始化、内存模型,以及数组常见陷阱和性能优化。通过图文结合的方式帮助开发者彻底理解数组本质,并提供Arrays工具类的实用方法与面试高频问题解析,助你掌握数组核心知识,避免常见错误。
|
存储 Java 索引
java 数组
在 Java 中,数组是一种数据结构,用于存储多个相同类型的数据元素。数组的大小一旦创建后就不能改变,因此它是固定长度的。Java 数组是一种 对象,即使它存储的值是基本类型(如 int、double 等),它也是一个对象引用。
332 0
|
存储 人工智能 Java
打乱数组内容引发的问题( Java)
本文介绍了两种实现数组随机打乱的方法,并深入探讨了Java中原始数据类型与对象类型的差异。方法一通过自定义随机数交换数组元素位置,方法二借助`Collections.shuffle()`函数完成数组打乱。同时,文章详细解析了`int`和`Integer`的区别,包括声明方式、内存占用、初始化以及对象特性等,并讲解了自动装箱与拆箱的功能,帮助读者更好地理解Java的基础知识。
321 0
|
存储 Java 数据挖掘
Java 中数组的多种定义方式
本文深入解析了Java中数组的多种定义方式,涵盖基础的`new`关键字创建、直接初始化、动态初始化,到多维数组、`Arrays.fill()`方法以及集合类转换为数组等高级用法。通过理论与实践结合的方式,探讨了每种定义方法的适用场景、优缺点及其背后的原理,帮助开发者掌握高效、灵活的数组操作技巧,从而编写更优质的Java代码。
889 0
|
人工智能 Java
Java 中数组Array和列表List的转换
本文介绍了数组与列表之间的相互转换方法,主要包括三部分:1)使用`Collections.addAll()`方法将数组转为列表,适用于引用类型,效率较高;2)通过`new ArrayList&lt;&gt;()`构造器结合`Arrays.asList()`实现类似功能;3)利用JDK8的`Stream`流式计算,支持基本数据类型数组的转换。此外,还详细讲解了列表转数组的方法,如借助`Stream`实现不同类型数组间的转换,并附带代码示例与执行结果,帮助读者深入理解两种数据结构的互转技巧。
1195 1
Java 中数组Array和列表List的转换
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
651 1
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
575 1