1. 程式人生 > >資料結構與演算法分析筆記與總結(java實現)--陣列11:陣列中的逆序對(﹡)

資料結構與演算法分析筆記與總結(java實現)--陣列11:陣列中的逆序對(﹡)

題目:在陣列中的兩個數字,如果前面一個數字大於後面的數字,則這兩個數字組成一個逆序對。輸入一個數組,求出這個陣列中的逆序對的總數P。並將P對1000000007取模的結果輸出。 即輸出P%1000000007

輸入描述:

題目保證輸入的陣列中沒有相同的數字

資料範圍:

       對於%50的資料,size<=10^4

       對於%75的資料,size<=10^5

       對於%100的資料,size<=2*10^5

思路:

方法1:遍歷陣列,每遍歷一個元素在遍歷這個元素後面的元素,時間複雜度為O(n^2)

方法2:歸併排序的思想(歸併排序利用的是分治的思想,由拆分和合並兩個步驟組成,拆分和合並都需要遞迴呼叫來實現)

即先將陣列不斷的分割成兩個部分,通過遞迴不斷的分成兩個部分,直到最後每一半隻有1個元素為止停止遞迴,顯然對於當個元素來說,其逆序對為0,然後逐一進行合併,7和5合併時由於left、right子陣列內部的逆序對都是0,因此只要計算合併時產生的逆序對數目即可,同時要求合併後的陣列排序,排序後在內部就沒有逆序對了,從而不會對後續的逆序對尋找差生影響,同時排序後對於合併時確定逆序對的數目很方便(這也是歸併排序中的做法,先二分再合併,共要進行logn次合併,每次合併時要進行排序,排序要遍歷兩個子陣列並記錄較小值從而需要消耗時間複雜度O(n)以及空間複雜度O(n),從而歸併排序的時間複雜度是O(nlogn),空間複雜度為O(n)。本題中空間複雜度為O(n),歸併排序優化後可以做到空間複雜度為O(1))。

每次合併merge方法中如何計算有多少逆序對?

已知左右兩個子陣列left,right是有序的,設定兩個指標,分別在左右兩個陣列的第一個元素leftPoint、rightPoint上面,比較兩個數值大小,如果leftPoint<rightPoint,表示這兩個數不構成逆序對,於是將leftPoint++,再次比較;如果leftPoint> rightPoint,說明這是一個逆序對,並且leftPoint後面的數都比leftPoint要大,所以與rightPoint都可以構成逆序對,所以由leftPoint產生的逆序對數目是leftPoint陣列中leftPoint以及後面元素的數目即middle-leftPoint+1;然後將rightPoint++再次比較leftPoint和rightPoint。當left、right兩個陣列遍歷完成後就可以知道在合併過程中產生的逆序對的數目;在leftPoint和rightPoint的比較過程中,除了計算逆序對數目之外還要對合並後的陣列排序,排序在leftPoint和rightPoint逐一比較的過程中進行,每次leftPoint和rightPoint比較,將較小的值放入到建立的temp[]陣列上面,如果left或者right一側的陣列提前遍歷完成,呢麼逆序對不再增加,將right或者left陣列中剩下的元素全部直接複製到temp[]中即可;最終當left和right陣列遍歷合完成後這個陣列對應在temp中就是有序的,由於下一次合併在array[]陣列基礎上進行而不是在temp基礎上進行,因此需要將temp[]從start到end的部分重新對array進行覆蓋。

理解:歸併排序分成“分”和“並”兩個部分,分別用divide()方法和merge()方法來實現功能,其中divide()方法是遞迴方法,merge()方法不是遞迴方法,因此在divide()方法中需要有終止遞迴迴圈的邊界條件,同時在divide()方法中要呼叫自身divide()方法和merge()方法實現下一層子陣列的拆分和合並,即在divide()方法裡面除了遞迴呼叫自身方法進行進一步的分割之外還要呼叫merge方法對分割後的子陣列進行合併。在主函式中,只要給定初始邊界條件,然後直接呼叫divide()方法即可解決問題,當然也可以在主函式中先自己分割一次得到left陣列和right陣列,在對兩個陣列遞迴呼叫divide方法再進行合併,但是這沒有必要,可以省略,如程式所示。此外還要注意取模不僅要在返回結果時取模還要在程式中用到count的地方都是用取模,避免溢位。

//找逆序對較複雜,這裡使用分治思想,利用歸併排序來找逆序對,就是在歸併排序的基礎上,在每次合併時對逆序對進行了統計而已,關鍵還是熟練使用遞迴

public class Solution {

    //定義一個成員變數用來統計逆序對的數目

    int count;

    public intInversePairs(int [] array) {

        //特殊輸入和邊界輸入

       if(array==null||array.length<=0) return 0;

        //使用遞迴方法不斷進行拆分和合並

        //建立一個數組用來在合併時儲存排序後的數

        int tempArray[]=newint[array.length];

        intmiddle=(0+array.length-1)/2;

        //對左陣列進行拆分合並得到左陣列的逆序對數目

       this.divide(array,tempArray,0,array.length-1);

        //①對右陣列進行拆分合並得到右陣列的逆序對數目,注意這裡①②不需要寫,直接寫divide就可以

       //this.divide(array,tempArray,middle+1,array.length-1);

        //②對左右兩個陣列進行合併計算合併時產生的逆序對數目

       //this.merge(array,tempArray,0,array.length-1);

       //count在方法呼叫的過程中不斷增長,方法結束時count就是結果,將其返回即可

        return count%1000000007;

    }

    //這個方法用來將陣列進行拆分,在start~end範圍之間進行拆分

    public voiddivide(int[] array,int[] tempArray,int start,int end){

        intmiddle=(start+end)/2;

        //左陣列範圍是start~middle;右陣列範圍是middle+1~end

        //拆分遞迴方法的終止條件

        if(start>=end)return;

        //如果沒有終止就遞迴呼叫繼續拆分

       this.divide(array,tempArray,start,middle);

       this.divide(array,tempArray,middle+1,end);

        this.merge(array,tempArray,start,end);

    }

    /*這個方法用來對兩個已經排序的子陣列進行合併,將其重新排序並且記錄合併時產生的逆序對數目,將在startend範圍之內的陣列進行合併,

    顯然這個兩個陣列的分界點是(start+end)/2*/

    public voidmerge(int[] array,int[] tempArray,int start,int end){

        intmiddle=(start+end)/2;

        //左側子陣列是從start~middle;右側子陣列是從middle+1~end

        //現將其合併排序,統計逆序對數目

       int leftPoint=start;

        intrightPoint=middle+1;

        //理解:每次呼叫merge()方法只是對startend範圍內的資料進行排序,因此用tempPoint指標來記錄臨時陣列中填充進的數字的位置

        inttempPoint=start;

        //對兩個子陣列進行遍歷,直到一個數組遍歷結束,防止陣列訪問越界

       while(leftPoint<=middle&&rightPoint<=end){

           //不構成逆序對,count不變,將較小的數值放入到temp陣列中

           if(array[leftPoint]<array[rightPoint]){

               tempArray[tempPoint]=array[leftPoint];

               leftPoint++;

                tempPoint++;

            }else{

               //構成逆序對,統計逆序對的數目,並將較小值放入到temp陣列中

               //注意細節:由於count可能很大,因此最後輸出時採用對10000000007取模作為輸出,實際上不僅在最後返回時可能溢位,在方法執行中操作

               //count時也可能發生溢位,因此在方法中任何用到count的地方,都使用取模後的值作為參與相加運算的值,避免中途溢位

               count=count%1000000007+((middle-leftPoint+1)%1000000007);

               tempArray[tempPoint]=array[rightPoint];

               rightPoint++;

               tempPoint++;

            }

        }

        //迴圈結束,表示有一個子陣列已經遍歷完成到達邊界,此時count不變,將另一個數組中的值全部複製到temp陣列中即可

       if(leftPoint<=middle){

           //右側陣列遍歷完成,將左側複製到temp即可

           while(leftPoint<=middle){

               tempArray[tempPoint]=array[leftPoint];

               leftPoint++;

               tempPoint++;

            }

        }elseif(rightPoint<=end){

           //左側的陣列遍歷完成,將右側剩餘數字複製到temp即可

           tempArray[tempPoint]=array[rightPoint];

           tempPoint++;

           rightPoint++;

        }

        //子陣列已經合併完成,count已經統計,此時將tempstartend部分的陣列覆蓋到array中,從而保證下一次合併時兩個子陣列是有序的,注意臨時陣列temp使用過後可以覆蓋。

        for(inti=start;i<=end;i++){

           array[i]=tempArray[i];

        }

    }

}

對一些小地方進行合併簡化之後的程式碼:

public class Solution {

    private long count;

    public intInversePairs(int [] array) {

       if(array==null||array.length==0)

            return 0;

        int[] temp=newint[array.length];

       divid(array,temp,0,array.length-1);

        return(int)(count%1000000007);

    }

    public void divid(int[]array,int[]temp,int low,int high){

       if(low>=high)

            return;

        intmid=(low+high)/2;

       divid(array,temp,low,mid);

       divid(array,temp,mid+1,high);

       merge(array,temp,low,high);

    }

    public void merge(int[]array,int[]temp,intlow,int high){

        intlowend=(low+high)/2;

        inthighpos=lowend+1;

        inttemppos=low;

        intlen=high-low+1;

       while(low<=lowend&&highpos<=high){

           if(array[low]<=array[highpos]){

                temp[temppos++]=array[low++];                       //簡化寫法,節省程式碼

            }else{

                count+=(lowend-low+1)%1000000007; 

//改為count=count%1000000007+((middle-leftPoint+1)%1000000007);才通過

               temp[temppos++]=array[highpos++];        //簡化寫法,節省程式碼

            }

        }

       while(low<=lowend){

           temp[temppos++]=array[low++];

        }

       while(highpos<=high){

           temp[temppos++]=array[highpos++];

        }

        for(inti=len;i>0;i--,high--){                                          //for迴圈中一個分號可以寫多個條件

           array[high]=temp[high];

        }

    }

}