1. 程式人生 > >MergeSort歸併排序和利用歸併排序計算出陣列中的逆序對

MergeSort歸併排序和利用歸併排序計算出陣列中的逆序對

  首先先上LeetCode今天的每日一題(面試題51. 陣列中的逆序對):

  在陣列中的兩個數字,如果前面一個數字大於後面的數字,則這兩個數字組成一個逆序對。輸入一個數組,求出這個陣列中的逆序對的總數。

//輸入: [7,5,6,4]
//輸出: 5
示例1

  由於題目中已經給出陣列長度為: 0 <= 陣列長度 <= 50000, 所以如果單純使用兩個for迴圈(時間複雜度為 $O\left ( n^{2} \right )$ 暴力求解的話是一定會超時的。

  在這裡可以使用歸併排序,並同時得出逆序對的總數,其中歸併排序使用的是“分治法”,所以時間複雜度為 $O\left ( nlogn \right )$ ,而計算逆序對只需要在每次迴圈中進行一次計算,所以相當於在其中增加 $O\left ( 1 \right )$ 的時間複雜度,所以時間複雜度並不會變化,這個在後面會對計算方法會有詳細的介紹,因為一開始自己寫的時候也有在這裡卡住。

  如下是歸併排序的示意圖,圖是盜來的,但是覺得做的真的是太好看了,而且清楚明瞭,以下超連結為引用的原部落格網址(https://www.cnblogs.com/chengxiao/p/6194356.html):

  相信看了這張圖之後,整個歸併排序的演算法就已經非常清楚明瞭了,如下程式碼是隻對於歸併排序的實現,使用的是遞迴的方法:

public class mergeSort {
    public static void main(String args[]) {
        mergeSort a = new mergeSort();
        int[] numbers = new int[] {7,2,5,2,6,3,4,8};
        a.merge(0, numbers.length-1, numbers);
        for (int i : numbers) {
            System.out.println(i);
        }
    }
    public void merge(int left, int right, int[] numbers) {
        if(left < right) {
            int mid = (left + right)/2;
            merge(left, mid, numbers);
            merge(mid+1, right, numbers);;    
            mergeSort(left, right, numbers);
        }
    }
    public void mergeSort(int left, int right, int[] numbers) {
        //將陣列分為左右兩個部分,分別為[left, mid]和[mid+1, right]
        int mid = (left + right)/2;
        int i = left;
        int j = mid + 1;
        int[] temp = new int[right - left + 1];
        for(int k = 0 ; k < temp.length; k++) {
            //考慮如果陣列左邊已經到達尾端,則只需要將右邊陣列依次放入temp陣列即可
            if(i == mid + 1) {
                temp[k] = numbers[j];
                j++;
            }
            //考慮如果陣列右邊已經到達尾端,則只需要將左邊陣列依次放入temp陣列即可
            else if(j == right + 1) {
                temp[k] = numbers[i];
                i++;
            }
            //如果左邊陣列指向的數字大於右邊陣列指向的數字,則將右邊陣列指向的數字放入temp陣列當中
            else if(numbers[i] > numbers[j]) {
                temp[k] = numbers[j];
                j++;
            }
            //反之亦然
            else {
                temp[k] = numbers[i];
                i++;
            }
        }
        //最後將排好序的temp陣列重新放入原陣列當中,記得起始位置是從numbers陣列的left開始,而不是0
        for(int m = left, k = 0; m <= right; m++, k++) {
            numbers[m] = temp[k];
        }
    }
}

//最終結果為:2,2,3,4,5,6,7,8
歸併排序的實現

  那麼,如何來計算出逆序對呢?那麼我們就要思考,為什麼在歸併排序中就能計算出逆序對的數量。這就要觀察每次用來排序的陣列的特點了,由於排序是由從兩個長度為1的陣列開始進行的,所以就可以保證在每一次的遞迴過程中,我們需要進行排序的陣列一定會有以下規律,即:

  1. 將要排序的陣列number的左右兩個部分一定都是已經分別排好序了的,例如上圖中需要排序的陣列[4,5,7,8,1,2,3,6], 將這個陣列分為左右兩個部分[4,5,7,8]和[1,2,3,6],這兩個陣列是一定已經排好順序了的。

  2. 每個數字與其他陣列都會正好比一次大小,例如上圖中的數字4,它在這次的統計中,會跟1,2,3,6比,會發現有3組逆序對,而在那之後,這個4就再也不會跟這4給數字進行比較了,也就不會產生重複。

  這時,你一定會問,那前面的5,7,8又是在什麼時候進行比較的呢?其實在上一步,即當大的陣列為[4,5,7,8]的時候,4就已經和7,8進行了比較,而在更前一步,4就和5進行了比較,所以就可以完美的不重複不遺漏統計所有數字的逆序對了。

  既然不會重複,那我們也就只需要有一個計數器count來記錄產生的逆序對的數量就行了,那麼,怎麼來計算這個逆序對的數量呢?

  我一開始的錯誤想法是,左邊陣列的每一個數字和右邊數字進行比較的時候,如果發現左邊數字大於右邊,那麼count就+1,但發現統計的數量總是少於實際值,後來才發現了原因:

  例如陣列[2,2,4,5,3,4,6,8], 將其看為左右兩個部分,可以發現當左邊陣列指標指向5的時候,右邊陣列的指標已經指向4了,那麼其實本來陣列中的5和3是並沒有進行比較的,因此就會出現漏數的情況。

  在觀察了很久之後,終於發現了其中的規律:

  假設左邊陣列指標為 $i$ , 右邊陣列指標為 $j$ , 如果發現 $ numbers[i] > numbers[j]$ , 那麼對於右邊陣列的這個數字 $numbers[j]$ ,一定有左邊陣列的 $[i, mid]$ 位置的數字都會大於這個數字,因為這裡兩邊的陣列都是遞增的,所以我們只需要每次發現 $numbers[i] > numbers[j]$ 之後,用 $count = count + (mid - i + 1)$ 統計即可。

  注意:為了統計count的數量,一定要將方法中的返回值型別從 void 變為 int, 因為如果只是利用void方法傳參的話,count的值是不會改變的。(如有錯誤,歡迎指正,應該是這個樣子的吧)

  最終利用歸併排序計算逆序對的程式碼實現如下:

public class Merge_Sort {
    public static void main(String args[]) {
        Merge_Sort a = new Merge_Sort();
        int[] numbers = new int[] {4,2,5,2,6,3,4,8};
        int b = a.merge(0, numbers.length-1, numbers);
        System.out.println(b);
    }
    public int merge(int left, int right, int[] numbers) {
        if(left < right) {
            int mid = (left + right)/2;
            int count = merge(left, mid, numbers) + merge(mid+1, right, numbers);;    
            return mergeSort(left, right, numbers, count);
        }
        return 0;
        
    }
    public int mergeSort(int left, int right, int[] numbers, int count) {
        int mid = (left + right)/2;
        int i = left;
        int j = mid + 1;
        int[] temp = new int[right - left + 1];
        for(int k = 0 ; k < temp.length; k++) {
            if(i == mid + 1) {
                temp[k] = numbers[j];
                j++;
            }
            else if(j == right + 1) {
                temp[k] = numbers[i];
                i++;
            }
            else if(numbers[i] > numbers[j]) {
                temp[k] = numbers[j];
                j++;
                //count計數程式碼新增如下
                count = count + (mid - i + 1);
            }
            else {
                temp[k] = numbers[i];
                i++;
            }
            
        }
        for(int m = left, k = 0; m <= right; m++, k++) {
            numbers[m] = temp[k];
        }
        return count;
    }
}
//輸出結果為8

&n