剑指Offer——数组中的逆序对(归并排序的应用)

蛮力:
遍历数组,对每个元素都往前遍历所有元素,如果有发现比它小的元素,就count++。
最后返回count取模。
结果没问题,但超时哈哈哈,只能过50%。
 
归并法:
看讨论,知道了这道题的经典做法应该是用归并的思想,之所以用归并,是因为像上面我们直接比较a[i]后面的所有元素的话,肯定是O(n^2)的,那么就先考虑它旁边周围的元素,然后就归并了。
 
好像之前也就写过归并的伪代码,所以我看了下大概思路,然后就开始写了,一开始是每次mergeSort方法里面,就要递归用的那个方法里面,都new两个temp数组,然后分别丢这两个数组给递归的sort方法,然后再把这两个排好序的数组合并到原来的数组a里面去。
 
测试的结果是通过了,但还是卡在百分之50……而且没给什么报错原因??50%数字很多也很大,给出的数组个数屏幕都不够显示……有个查看全部按钮可以在另一个页面查看数据。
于是我复制了这个挂了的数据,还有别人通过的方法。
测了下,结果一样啊????想了下可能是空间复杂度超了……毕竟我这里每次递归都搞了个数组……按道理这个归并排序应该是O(n)空间复杂度的。
 
上一下这个初始版本,也就是用了好多辅助数组的归并吧:
public class Solution {
    private int count = 0;
    
    /*讨论说可以用分治法——归并排序的思路喔*/
    public int InversePairs(int [] array) {
        if(array.length <= 0)return 0;
        mergeSortAndCount(array);
        return count % 1000000007;
    }
    
    private void mergeSortAndCount(int[] a) {
        if(a.length == 1)return;
        int[] b, c;
        if(a.length % 2 == 0) {//数组元素个数为偶数的情况
            b = new int[a.length / 2];
            c = new int[a.length / 2];
            for(int i = 0, j = a.length / 2, k = 0; i < a.length / 2 && j < a.length; i++, j++, k++) {
                b[k] = a[i];
                c[k] = a[j];
            }
        } else {
            b = new int[a.length / 2 + 1];
            c = new int[a.length / 2];
            for(int i = 0, j = a.length / 2 + 1, k = 0; i <= a.length / 2 || j < a.length; i++, j++, k++) {
                //这里b数组要初始化的长度是长一点的
                b[k] = a[i];
                if(j < a.length)c[k] = a[j];
            }
        }
        
        mergeSortAndCount(b);
        mergeSortAndCount(c);
        
        mergeAndCount(b, c, a);//把已经排好序的b和c数组合并并且计算逆序,合并到a数组中去。
        
    }
    
    private void mergeAndCount(int[] b, int[] c, int[] a) {
        int i = 0, j = 0, k = 0;
        while(i < b.length && j < c.length) {
            if(b[i] <= c[j]) {//正常情况,非逆序
                a[k ++] = b[i ++];
            } else  {//b的元素大于c的情况,逆序
                count = count + b.length - i;//因为这个数组是排好序的,b[i]元素大于c[j],意味着b[i]-b[a.length - 1]的元素都大于它
                a[k ++] = c[j ++];
            }
        }
        while(i < b.length)a[k ++] = b[i ++];
        while(j < c.length)a[k ++] = c[j ++];
    }
}

改造后的归并:

这里全程就用到一个辅助数组,和一个本身就要排序的数组。

内部在处理递归的时候用的都是index,然后要说明的是,辅助数组和排序数组的身份是互换的。本来应该是:在原数组的两部分排序好的数组要合并嘛,就借一个辅助数组放合并的数据,然后再复制回原来的数组。

但现在我们用了一个整数i,来交替完成这个过程,反正只要最后一次合并是合并到要排序的那个数组就好了嘛。

这里有个也是只用了一个辅助数组,但不是交替用的例子,这个其实好理解点hh:

https://blog.csdn.net/abc7845129630/article/details/52740746

然后上代码:

public class Solution {
    private long count = 0;//类变量,逆序对的数量
    
    /*减少辅助数组的归并法,或者说只用一个辅助数组的归并法*/
    public int InversePairs(int [] array) {
        if(array.length <= 0)return 0;
        int i = 0;
        int[] help = new int[array.length];//辅助数组,就用这一个就够了,所以空间复杂度为O(n)
        mergeAndCountSort(array, 0, array.length - 1, i, help);
        return (int)(count % 1000000007);
    }
    
    
   /**
    * 对a数组中的begin到end元素进行归并排序,如果i是奇数则排序后的数组合并到b数组中去(b[begin-end]有序);否则就存到a数组中去(a[begin-end]有序)
    * 之所以这样做是因为可以避免利用了辅助数组后还有复制元素到原来数组的操作
    * 因为我们最后要排序的是a数组嘛,所以在主程序中调用这个sort应该传i=0或者是其他偶数
    * @param a
    * @param begin
    * @param end
    * @param i
    * @param b
    */
    private void mergeAndCountSort(int[] a, int begin, int end, int i, int[] b) {
        if (begin == end) {// 递归终结条件
            // 一开始是没有加这句的,如果排序的数组是偶数个的话,就没事,奇数个的话就有事。
            if (i % 2 == 1) b[begin] = a[begin];// 奇数,这个结果要到b数组中去,否则不用动
        } else {

            int middle = (begin + end) / 2;// 中间index
            
            // 分别对左半部分和右半部分做递归的归并排序,i + 1保证了下次用另一个数组做辅助数组
            mergeAndCountSort(a, begin, middle, i + 1, b);
            mergeAndCountSort(a, middle + 1, end, i + 1, b);

            if (i % 2 == 1) {
                // i是奇数,那么归并后数组要合并到b数组中去
                mergeAndCount(a, begin, middle, end, b);
            } else {
                // i是偶数,那么归并后的数组要合并到a数组去
                mergeAndCount(b, begin, middle, end, a);
            }
        }
    }
    
    /**
     * 带有数逆序对的合并有序数组的方法,合并的过程中顺便数逆序对
     * 将a数组中的[begin-middle]有序对和[middle + 1-end]有序对合并,合并结果到result数组中去,result数组的[begin-end]有序
     * @param a
     * @param begin
     * @param middle
     * @param end
     * @param result
     */
    private void mergeAndCount(int[] a, int begin, int middle, int end, int[] result) {
        int i = begin, j = middle + 1, k = begin;
        while(i <= middle && j <= end) {
            if(a[i] <= a[j]) {
                //左边的元素小于右边的,正常情况,不是逆序对
                result[k++] = a[i++];
            } else {
                //左边的元素大于右边的,逆序情况
                
                //因为这个数组是排好序的,所以意味着左边部分后面的元素也大于这个右边部分的这个元素,然后这里要提前加上去
                //因为小的元素要被加到result中去,等等其他的就不会碰到它了
                count = count + (middle - i + 1);
                result[k++] = a[j++];
                
            }
        }
        
        while(i <= middle) result[k++] = a[i++];
        while(j <= end) result[k++] = a[j++];
    }
}

我刚改完这个归并的时候,在本地跑可以,然后排序也有效果,结果特么还是一样的结果????

%50一样卡在那个位置……我佛了。

查了很久,结合那个数据猜发现,这个给的例子就算是在新的网页中打开,也是还没结束的:

也就是说,这个逆序对的个数,应该是可以超过Int的范围的……

所以就像上面贴的代码一样,把int的count改成long,返回的时候再转换成int才通过。

这里讲一下这个归并法的一些要注意的地方:

1. 就是刚刚说的,逆序个数会大于int的最大值的问题,要用long。

2. 在递归中,本来begin == end就可以返回了,因为就有序了嘛,但这里因为涉及到复制来复制去,还有加个i的判断,如果为奇数要把这个数搞到b数组中去。(测试过,如果待排序的数字个数是偶数个好像就没有问题,是奇数个就会有问题)

3. 就是在数逆序对的时候的问题了,因为是排好序的,而且小的那个会进入目标数组排队,所以发现了左边的元素大于右边的元素时,不是只count++:

else {
                //左边的元素大于右边的,逆序情况
                
                //因为这个数组是排好序的,所以意味着左边部分后面的元素也大于这个右边部分的这个元素,然后这里要提前加上去
                //因为小的元素要被加到result中去,等等其他的就不会碰到它了
                count = count + (middle - i + 1);
原文地址:https://www.cnblogs.com/wangshen31/p/10657493.html