資料結構與演算法分析筆記與總結(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);
}
/*這個方法用來對兩個已經排序的子陣列進行合併,將其重新排序並且記錄合併時產生的逆序對數目,將在start和end範圍之內的陣列進行合併,
顯然這個兩個陣列的分界點是(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()方法只是對start到end範圍內的資料進行排序,因此用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已經統計,此時將temp中start到end部分的陣列覆蓋到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];
}
}
}