歸並排序(MergeSort)和快速排序(QuickSort)的一些總結問題


歸並排序(MergeSort)和快速排序(QuickSort)都是用了分治算法思想。

所謂分治算法,顧名思義,就是分而治之,就是將原問題分割成同等結構的子問題,之后將子問題逐一解決后,原問題也就得到了解決。

同時,歸並排序(MergeSort)和快速排序(QuickSort)也代表了兩類分治算法的思想。

對於歸並排序,我們對於待處理序列怎么分的問題上並沒有太多關注,僅僅是簡單地一刀切,將整個序列分成近乎均勻的兩份,然后將子序列進行同樣處理。但是,我們更多的關注點在於怎么把分開的部分合起來,也就是merge的過程。

對於快速排序來說,我們則是花了很大的功夫放在了怎么分這個問題上,我們設定了樞軸(標定點),然后通過partition的過程將這個樞軸放在合適的位置,這樣我們就不用特別關心合起來的過程,只需要一步一步地遞歸下去即可。

下面說兩個從歸並排序和快速排序所衍生出來的問題。

1)關於求一個數組中逆序對數量的問題


 

在一個數組中,

隨機取出兩個元素,例如取出的是2和3,根據它們原來的位置順序看,它們是有序的,那么這個數字對就稱為順序對。

當取出的是2和1時,根據它們原來的位置順序看,2排在1的前面,而2卻比1要大,這樣的數字對稱為逆序對。

一個數組中逆序對的數量,可以用來衡量一個數組的有序程度。

那么怎么求一個數組中逆序對的數量呢?

一個最簡單的方法就是暴力解法:考察每一個數字對(使用雙重循環),算法復雜度為O(n2

我們還可以使用歸並算法進行考察逆序對的個數,使得我們的算法復雜度到達O(nlog2n)級別的。

使用歸並算法,最關鍵的是歸並的過程。

舉個例子:

在這個序列的歸並排序中,歸並過程我們首先比較紫色部分的2和1的大小,由於1比2小,所以我們把1放到最前端的位置。

由於每個子序列在歸並之前都是有序的,既然1比2小,那么1也一定比第一個子序列中2后面的所有元素小,換句話說,這個1比前面子序列中2以及2之后的所有元素都構成了逆序對。所以在將1放到整個序列最前端的過程中,我們就可以給逆序對的計數器加上4。

接下來比較2和4:

這里2比4要小,那么就把2放到1的后面。

2比4小,還意味着2比4后面的所有元素都要小。那么此時就沒有構成任何逆序對。計數器不動,繼續歸並。

以此類推。

這樣我們就把暴力解法中的一對一對的比較變成了一組一組的比較。

代碼:

package com.mergeSort;

import java.util.*;

public class InversionCount{

    // 我們的算法類不允許產生任何實例
    private InversionCount(){}

    // merge函數求出在arr[l...mid]和arr[mid+1...r]有序的基礎上, arr[l...r]的逆序數對個數
    private static long merge(int[] arr, int l, int mid, int r) {

        int[] aux = Arrays.copyOfRange(arr, l, r+1);//注意復制后的數組元素包括前索引位置的元素,不包括后索引位置的元素

        // 初始化逆序數對個數 res = 0
        long res = 0L;
        // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
        int i = l, j = mid+1;
        for( int k = l ; k <= r; k ++ ){

            if( i > mid ){  // 如果左半部分元素已經全部處理完畢
                arr[k] = aux[j-l];//注意有l個偏移量
                j ++;
            }
            else if( j > r ){   // 如果右半部分元素已經全部處理完畢
                arr[k] = aux[i-l];
                i ++;
            }
            else if( aux[i-l]<=aux[j-l] ){  // 左半部分所指元素 <= 右半部分所指元素
                arr[k] = aux[i-l];
                i ++;
            }
            else{   // 右半部分所指元素 < 左半部分所指元素
                arr[k] = aux[j-l];
                j ++;
                // 此時, 因為右半部分k所指的元素小
                // 這個元素和左半部分的所有未處理的元素都構成了逆序數對
                // 左半部分此時未處理的元素個數為 mid - i + 1
                res += (long)(mid - i + 1);
            }
        }

        return res;
    }

    // 求arr[l..r]范圍的逆序數對個數
    // 思考: 歸並排序的優化可否用於求逆序數對的算法? :)
    private static long solve(int[] arr, int l, int r) {

        if (l >= r)
            return 0L;

        int mid = l + (r-l)/2;
        // 求出 arr[l...mid] 范圍的逆序數
        long res1 = solve(arr, l, mid);
        // 求出 arr[mid+1...r] 范圍的逆序數
        long res2 = solve(arr, mid + 1, r);
     //只有每一次merge才會返回逆序數,而最底層的res(即solve()方法的返回值)都為0
     //所以這一句最后加的總和其實就是每次merge的返回值
return res1 + res2 + merge(arr, l, mid, r); } public static long solve(int[] arr){ int n = arr.length; return solve(arr, 0, n-1); } // 測試 InversionCount public static void main(String[] args) { int[] arr=new int[]{1,2,3,5,4,4}; long l=solve(arr); System.out.println(l); return; } }

2)取出數組(無序)中第n個大的元素


 

最簡單的實例就是求數組中的最大值和最小值。這需要我們從頭到尾遍歷掃描一下即可。時間復雜度為O(n)。

那么怎么求數組中第n個大的元素呢?

容易想到的一個就是給整個數組排一下序,時間復雜度為:O(nlog2n)。

其實我們可以使用快速排序算法的思想來求解這個問題,來使得時間復雜度達到O(n)級別。

在快速排序中,每一次我們都需要找到一個標定點,然后將這個標定點放到合適的位置,這個合適的位置就是這個元素在排好序后最終應該在的位置。那么從它的索引就能看出它是第幾大的元素。

例如,

在排完第一個元素時,我們發現一個元素4恰好是第4名,也就是說4這個元素在這個序列中是第4大的。那么現在問題是:請問這個序列第6大的元素是誰?

此時我們就不用去管元素4前面的位置了,只需要遞歸地去求解元素4后面的元素即可。

同樣的,如果問題是:請問這個序列第2大的元素是誰?

那么我們只需要遞歸地去求解元素4前面的元素即可。

在不太規范的統計下,時間復雜度為:

代碼:

package com.quickSort;

import java.util.*;

public class QuickSortWhoBig2 {

    // 我們的算法類不允許產生任何實例
    private QuickSortWhoBig2(){}

    // 對arr[l...r]部分進行partition操作
    // 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
    // partition 過程, 和快排的partition一樣
    private static int partition(int[] arr, int l, int r){

        int v = arr[l];

        int j = l; // arr[l+1...j] < v ; arr[j+1...i) > v
        for( int i = l + 1 ; i <= r ; i ++ )
            if( arr[i] < v ){
                j ++;
                int tem=arr[i];
                arr[i]=arr[j];
                arr[j]=tem;
            }

        int tem=arr[l];
        arr[l]=arr[j];
        arr[j]=tem;

        return j;
    }

    // 求出nums[l...r]范圍里第k小的數
    private static int solve(int[] nums, int l, int r, int k){

        if( l == r )
            return nums[l];

        // partition之后, nums[p]的正確位置就在索引p上
        int p = partition(nums, l, r);

        if( k == p )    // 如果 k == p, 直接返回nums[p]
            return nums[p];
        else if( k < p )    // 如果 k < p, 只需要在nums[l...p-1]中找第k小元素即可
            return solve( nums, l, p-1, k);
        else // 如果 k > p, 則需要在nums[p+1...r]中找第k-p-1小元素
             // 注意: 由於我們傳入QuickSortWhoBig2的依然是nums, 而不是nums[p+1...r],
             //       所以傳入的最后一個參數依然是k, 而不是k-p-1
            return solve( nums, p+1, r, k );
    }

    // 尋找nums數組中第k小的元素
    // 注意: 在我們的算法中, k是從0開始索引的, 即最小的元素是第0小元素, 以此類推
    // 如果希望我們的算法中k的語意是從1開始的, 只需要在整個邏輯開始進行k--即可, 可以參考solve2
    public static int solve(int nums[], int k) {

        return solve(nums, 0, nums.length - 1, k);
    }

    // 尋找nums數組中第k小的元素, k從1開始索引, 即最小元素是第1小元素, 以此類推
    public static int solve2(int nums[], int k) {

        return QuickSortWhoBig2.solve(nums, k - 1);
    }


    // 測試 QuickSortWhoBig2
    public static void main(String[] args) {
        int[] arr=new int[]{10,9,8,7,6,5,4,3,2,1};
        int n=1;
        for(int i=0;i<10;i++){
            System.out.println("第"+n+"大的元素為:"+solve2(arr,n));
            n++;
        }

    }
}

輸出結果:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM