基礎算法 | 最終章-8 歸併排序

我們已經在本系列文章中已經學習了7種算法,其中一種是查找算法,六種是排序算法。本篇文章是基礎算法系列的最後一章,我們將學習最後一個排序算法——歸併排序。讓我們話不多說,開始學習吧~


歸併排序

歸併排序是一種效率較高的排序,它用到了我們算法設計方法裏面的分治算法(在後面的新主題文章會講述),在處理大量排序數據時,歸併排序的效率比我們之前所學的冒泡排序,直接插入排序等算法要快很多。歸併排序,我們從這個名字中似乎就能看出其算法思想——歸併嘛,那怎麼個歸併法呢?那讓我們來詳細看看其思想吧。


歸併排序的算法思想

我們先想想這樣一個問題,假設我們有兩個有序的數列,我們怎麼把這兩個數列合併成一個有序的數列呢?我們可以這樣做,我們分別從兩個數列的第一個元素開始,相互比較,因爲這兩個數列都是有序的,所以比較之後,將小的元素臨時插入到一個第三個數列的第一個元素,然後將這個小的元素從所屬的數列中"去掉",然後取這個數列的下一個元素和另外一個數列的那個較大(此處爲第一個)元素進行比較,同樣,我們將小的元素臨時插入到第三個數列的第二個位置,以此類推。經過這樣一輪比較之後,開始的兩個數列肯定會有一個數列"剩餘"元素,我們將剩餘的元素依次插入到上述的第三個數列中,這樣,這個第三個數列就是我們想要的那個合併以後的數列。解決了這個問題之後,我們就知道了歸併排序的算法思想,即我們可以將待排序的數列中每一個單一的元素看成一個有序的數列,然後將它們兩兩按上述方式合併得到了n/2(假設有n個元素)個有序數列,再將其兩兩合併,重複此過程直到最後合併成一個數列,不就得到了我們所需要的有序數列嘛~


歸併排序的實現過程

假設待排序數列a長度爲n,第一個元素索引位置爲start,最後一個爲end。我們可以將數列"分割"爲n個有序數列,然後再通過merge函數兩兩合併,得到我們需要的結果。那我們怎麼"分割"呢?對了,我們通過遞歸來實現,我們每次取中間位置mid=(start+end)/2,然後對a[start~mid]與a[mid+1~end]兩部分元素分別進行劃分,這樣通過不斷遞歸,劃分數組直到每個數列只有一個元素,然後在進行不斷地兩兩合併,得到結果。此外,我們還需考慮一個問題,就是每次兩兩合併時我們都需要一個第三個臨時數列用來儲存結果,如果每次合併時都新產生一個數列,這樣會十分影響算法的效率。所以我們可以在算法中預先定義一個temp數組,讓每次合併的時候都共享這個數組。


代碼實現

public static void mergeSort(int[]a ,int start,int end){ //對下面的mergeSort方法進行封裝
        int[] temp = new int[a.length];  //定義中間變量temp數組
        mergeSort(a, temp, start, end);  //調用未進行封裝的mergeSort方法
    }

    public static void mergeSort(int[]a,int[] temp,int start,int end){
        if(start < end){   //當待排序元素的長度大於1時
            int mid = (start+end)/2;
            mergeSort(a, temp, start, mid);  //對 待排序的元素左邊進行排序
            mergeSort(a, temp, mid+1, end); //對 待排序的元素右邊進行排序
            merge(a,temp,start,mid,end);  //合併左右兩邊元素,即a[start~mid]與a[mid+1~end]兩部分元素進行合併
        }
    }

   //對a[start~mid]與a[mid+1~end]兩部分元素進行合併
    public static void merge(int[]a,int[] temp,int start,int mid,int end){
//對兩部分元素start位置進行標記,即從第一個(最小的)元素開始(此處也可以標記末端,從最大的元素開始,並修改下面的比較)
        int i =start,j=mid+1;
        int k = 0; //標記中間值temp數組的位置
        while(i <= mid && j <= end){  //當兩部分元素都未到達末端時
            if(a[i]<a[j]){  //若a[i]<a[j],將a[i]插入到temp中,之後標記i,k向後移動一個位置
                temp[k] = a[i];
                i++;
                k++;
            }
            else{ //若a[i]>=a[j],將a[j]插入到temp中,之後標記j,k向後移動一個位置
                temp[k] = a[j];
                j++;
                k++;
            }
        }
        if(i == mid+1){ //當i等於length1時,則將後半部分剩餘的元素全部依次插入到temp的後面
            while(j<=end){
                temp[k] = a[j];
                j++;
                k++;
            }
        }
        if(j == end+1){ //當j等於length2時,則將前半部分剩餘的元素全部依次插入到temp的後面
            while(i<=mid){
                temp[k] = a[i];
                i++;
                k++;
            }
        }
        //將temp中排好序的元素依次賦值給數組a對應位置元素
        for(int x =0;x<k;x++){
            //注意此處應爲a[start+x],因爲a[start~mid]與a[mid+1~end]兩部分元素中start不一定是從索引0開始的
            a[start+x] = temp[x];
        }

    }

讓我們運行下面代碼,來測試下我們的算法吧

public static void main(String[] args) {
        int[] a = new int[]{4,16,79,20,30,16,52,2,1};
        mergeSort(a,0, a.length-1);
        System.out.println(Arrays.toString(a));//打印出數組a
    }

結果爲:

又到了我們最熟悉的環節了,上題咯~


HDU 2561 第二小整數

Problem Description 求n個整數中倒數第二小的數。 每一個整數都獨立看成一個數,比如,有三個數分別是1,1,3,那麼,第二小的數就是1。

Input 輸入包含多組測試數據。 輸入的第一行是一個整數C,表示有C測試數據; 每組測試數據的第一行是一個整數n,表示本組測試數據有n個整數(2<=n<=10),接着一行是 n個整數 (每個數均小於100);

Output 請爲每組測試數據輸出第二小的整數,每組輸出佔一行。

Sample Input 2 2 1 2 3 1 1 3

Sample Output 2 1

分析:是不是感覺有上面我們測試例子的影子呢,只要對每組數進行排序,找出按升序排序的第二個數即可。剛剛我們學的歸併排序就可以派上用場了。

代碼實現:

public static void main(String[] args) {
        int n,m; //n組測試數據,m爲每一組數據的個數
        Scanner input = new Scanner(System.in);
        System.out.println("請輸入數據的組數:");
        n = input.nextInt();
        int[] result = new int[n];  //記錄每一組的結果
        for(int i=0;i<n;i++){
            System.out.println("請輸入本組測試數據的個數:");
            m=input.nextInt();
            int[] a = new int[m];//保存本組數據
            System.out.println("請輸入本組測試數據:");
            for(int j=0;j<m;j++){
                a[j] =input.nextInt();
            }
            mergeSort(a, 0, m-1); //對本組數據進行排序
            result[i] = a[1];   //獲取第二小的整數
        }
        for(int i :result){
            System.out.println(i+"  ");
        }
    }

    public static void mergeSort(int[]a ,int start,int end){ //對下面的mergeSort方法進行封裝
        int[] temp = new int[a.length];  //定義中間變量temp數組
        mergeSort(a, temp, start, end);  //調用未進行封裝的mergeSort方法
    }

    public static void mergeSort(int[]a,int[] temp,int start,int end){
        if(start < end){   //當待排序元素的長度大於1時
            int mid = (start+end)/2;
            mergeSort(a, temp, start, mid);  //對 待排序的元素左邊進行排序
            mergeSort(a, temp, mid+1, end); //對 待排序的元素右邊進行排序
            merge(a,temp,start,mid,end);  //合併左右兩邊元素,即a[start~mid]與a[mid+1~end]兩部分元素進行合併
        }
    }
    /**
     * 對a[start~mid]與a[mid+1~end]兩部分元素進行合併
     */
    public static void merge(int[]a,int[] temp,int start,int mid,int end){
//對兩部分元素start位置進行標記,即從第一個(最小的)元素開始(此處也可以標記末端,從最大的元素開始,並修改下面的比較)
        int i =start,j=mid+1;
        int k = 0; //標記中間值temp數組的位置
        while(i <= mid && j <= end){  //當兩部分元素都未到達末端時
            if(a[i]<a[j]){  //若a[i]<a[j],將a[i]插入到temp中,之後標記i,k向後移動一個位置
                temp[k] = a[i];
                i++;
                k++;
            }
            else{ //若a[i]>=a[j],將a[j]插入到temp中,之後標記j,k向後移動一個位置
                temp[k] = a[j];
                j++;
                k++;
            }
        }
        if(i == mid+1){ //當i等於length1時,則將後半部分剩餘的元素全部依次插入到temp的後面
            while(j<=end){
                temp[k] = a[j];
                j++;
                k++;
            }
        }
        if(j == end+1){ //當j等於length2時,則將前半部分剩餘的元素全部依次插入到temp的後面
            while(i<=mid){
                temp[k] = a[i];
                i++;
                k++;
            }
        }
        //將temp中排好序的元素依次賦值給數組a對應位置元素
        for(int x =0;x<k;x++){
            //注意此處應爲a[start+x],因爲a[start~mid]與a[mid+1~end]兩部分元素中start不一定是從索引0開始的
            a[start+x] = temp[x];
        }

    }

總述

歸併排序是一種效率較高的排序算法,尤其在處理大量數據排序時有較大的優勢。它用到了我們算法設計方法裏面的分治算法(將在新主題文章中講述,敬請期待~)。你是否get到了呢~ヾ(◍°∇°◍)ノ゙

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章