劍指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,而後要說明的是,輔助數組和排序數組的身份是互換的。原本應該是:在原數組的兩部分排序好的數組要合併嘛,就借一個輔助數組放合併的數據,而後再複製回原來的數組。spa

但如今咱們用了一個整數i,來交替完成這個過程,反正只要最後一次合併是合併到要排序的那個數組就行了嘛。.net

 

這裏有個也是隻用了一個輔助數組,但不是交替用的例子,這個其實好理解點hh:code

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

 

而後上代碼:排序

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同樣卡在那個位置……我佛了。io

查了好久,結合那個數據猜發現,這個給的例子就算是在新的網頁中打開,也是還沒結束的:class

也就是說,這個逆序對的個數,應該是能夠超過Int的範圍的……

因此就像上面貼的代碼同樣,把int的count改爲long,返回的時候再轉換成int才經過。

 

這裏講一下這個歸併法的一些要注意的地方:

1. 就是剛剛說的,逆序個數會大於int的最大值的問題,要用long。

2. 在遞歸中,原本begin == end就能夠返回了,由於就有序了嘛,但這裏由於涉及到複製來複制去,還有加個i的判斷,若是爲奇數要把這個數搞到b數組中去。(測試過,若是待排序的數字個數是偶數個好像就沒有問題,是奇數個就會有問題)

3. 就是在數逆序對的時候的問題了,由於是排好序的,並且小的那個會進入目標數組排隊,因此發現了左邊的元素大於右邊的元素時,不是隻count++:

else {
                //左邊的元素大於右邊的,逆序狀況
                
                //由於這個數組是排好序的,因此意味着左邊部分後面的元素也大於這個右邊部分的這個元素,而後這裏要提早加上去
                //由於小的元素要被加到result中去,等等其餘的就不會碰到它了
                count = count + (middle - i + 1);
相關文章
相關標籤/搜索