MergeSort歸併排序和利用歸併排序計算出數組中的逆序對

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

  在數組中的兩個數字,若是前面一個數字大於後面的數字,則這兩個數字組成一個逆序對。輸入一個數組,求出這個數組中的逆序對的總數。面試

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

  因爲題目中已經給出數組長度爲: 0 <= 數組長度 <= 50000, 因此若是單純使用兩個for循環(時間複雜度爲 $O\left ( n^{2} \right )$ 暴力求解的話是必定會超時的。算法

  在這裏可使用歸併排序,並同時得出逆序對的總數,其中歸併排序使用的是「分治法」,因此時間複雜度爲 $O\left ( nlogn \right )$ ,而計算逆序對只須要在每次循環中進行一次計算,因此至關於在其中增長 $O\left ( 1 \right )$ 的時間複雜度,因此時間複雜度並不會變化,這個在後面會對計算方法會有詳細的介紹,由於一開始本身寫的時候也有在這裏卡住數組

  以下是歸併排序的示意圖,圖是盜來的,可是以爲作的真的是太好看了,並且清楚明瞭,如下超連接爲引用的原博客網址(http://www.javashuo.com/article/p-nkdsmljg-cd.html):ide

  相信看了這張圖以後,整個歸併排序的算法就已經很是清楚明瞭了,以下代碼是隻對於歸併排序的實現,使用的是遞歸的方法:spa

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],這兩個數組是必定已經排好順序了的。code

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

  這時,你必定會問,那前面的5,7,8又是在何時進行比較的呢?其實在上一步,即當大的數組爲[4,5,7,8]的時候,4就已經和7,8進行了比較,而在更前一步,4就和5進行了比較,因此就能夠完美的不重複不遺漏統計全部數字的逆序對了。blog

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

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

  例如數組[2,2,4,534,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
相關文章
相關標籤/搜索