1. 程式人生 > 其它 >(五) 資料結構 - 歸併排序

(五) 資料結構 - 歸併排序

技術標籤:資料結構歸併排序排序演算法資料結構

歸併排序

歸併排序是一種基於分而治之的排序技術。最壞情況下的時間複雜度為O(nlogn),它是最受人尊敬的演算法之一。歸併排序首先將陣列分成相等的兩半,然後以排序的方式將它們合併。

核心思想

為了理解合併排序,我們採用未排序的陣列,如下所示
在這裡插入圖片描述
我們知道歸併排序首先將整個陣列迭代地分成相等的一半,除非獲得原子值。我們在這裡看到一個由8個專案組成的陣列分為兩個大小為4的陣列。
在這裡插入圖片描述
這不會更改原件中專案出現的順序。現在我們將這兩個陣列分為兩半。
在這裡插入圖片描述
我們進一步劃分這些陣列,並獲得無法再劃分的原子值
在這裡插入圖片描述
現在,我們將它們分解時的方式完全相同。請注意提供給這些列表的顏色程式碼。

我們首先比較每個列表的元素,然後以排序的方式將它們組合到另一個列表中。我們看到14和33處於排序位置。我們比較27和10,在2個值的目標列表中,我們先放置10,然後是27。我們更改19和35的順序,而將42和44順序放置。
在這裡插入圖片描述
在合併階段的下一個迭代中,我們比較兩個資料值的列表,然後將它們合併為找到的資料值的列表,將所有資料按排序順序放置。
在這裡插入圖片描述
最終合併後,列表應如下所示:
在這裡插入圖片描述

程式碼開發

實現思路

歸併排序會繼續將列表分為相等的一半,直到無法再對其進行劃分為止。根據定義,如果它只是列表中的一個元素,則會對其進行排序。然後,合併排序將合併較小的排序列表,同時也將新列表排序。

Step 1−如果列表中的元素已被排序,則返回。
Step 2−將列表遞迴分為兩半,直到無法再將其劃分為止。
Step 3−將較小的列表按排序順序合併到新列表中。

虛擬碼

package com.paal.demo.c01SortingBasic;

import com.paal.demo.Sort;

import java.util.Arrays;

/**
 * <p/>
 * <li>title: 基礎排序-歸併排序</li>
 * <li>@author: li.pan</li>
 * <li>Date: 2019/12/7 12:15 下午</li>
 * <li>Version: V1.0</li>
 * <li>Description: </li>
 */
public class MergeSort implements Sort { @Override public void sort(Integer[] arr) { int n = arr.length; sort(arr, 0, n - 1); } //遞迴使用歸併排序,對arr[l....r]的範圍進行進行排序 private static void sort(Integer[] arr, int l, int r) { if (l >= r) //當子序列中只有一個元素遞迴到底的情況 return; int mid = (l + r) / 2; sort(arr, l, mid); sort(arr, mid + 1, r); merge(arr, l, mid, r); } //將arr[l...mid]和arr[mid+1...r]兩部分進行歸併 private static void merge(Integer[] arr, int l, int mid, int r) { // 開闢臨時空間,合併左半部分已經排好序的陣列和右半部分已經排好序的陣列 Integer[] aux = Arrays.copyOfRange(arr, l, r + 1); // 初始化,i指向左半部分的起始位置索引;j指向右半部分起始索引位置mid+1 int i = l, j = mid + 1; for (int k = l; k <= r; k++) { // k指向兩個元素比較後歸併下一個需要放置的位置 /** * 考慮陣列越界 */ if (i > mid) { // 如果左半部分元素已經全部處理完畢, arr[k] = aux[j - l]; j++; } else if (j > r) { // 如果右半部分元素已經全部處理完畢 arr[k] = aux[i - l]; i++; // l表示偏移 } /** * 真正比較 */ else if (aux[i - l] < aux[j - l]) { // 左半部分所指元素 < 右半部分所指元素 arr[k] = aux[i - l]; i++; } else { // 左半部分所指元素 >= 右半部分所指元素 arr[k] = aux[j - l]; j++; } } } } /** * 希爾排序 */ @Test public void mergeSortTest() { Integer[] integers0 = SortTestHelper.generateRandomArray(100, 0, 1000000); Integer[] integers1 = SortTestHelper.generateRandomArray(10000, 0, 1000000); Integer[] integers2 = SortTestHelper.generateRandomArray(100000, 0, 1000000); System.out.println("------------------------------隨機陣列--------------------------------"); System.out.println("插入排序測試1資料量為100"+SortTestHelper.testSort(integers0, new InsertionSort())); System.out.println("插入排序測試2資料量為10000"+SortTestHelper.testSort(integers1, new InsertionSort())); System.out.println("插入排序測試3資料量為100000"+SortTestHelper.testSort(integers2, new InsertionSort())); System.out.println("氣泡排序測試1資料量為100"+SortTestHelper.testSort(integers0, new BubbleSort())); System.out.println("氣泡排序測試2資料量為10000"+SortTestHelper.testSort(integers1, new BubbleSort())); System.out.println("氣泡排序測試3資料量為100000"+SortTestHelper.testSort(integers2, new BubbleSort())); System.out.println("希爾排序測試1資料量為100"+SortTestHelper.testSort(integers0, new ShellSort())); System.out.println("希爾排序測試2資料量為10000"+SortTestHelper.testSort(integers1, new ShellSort())); System.out.println("希爾排序測試3資料量為100000"+SortTestHelper.testSort(integers2, new ShellSort())); System.out.println("歸併排序測試1資料量為100"+SortTestHelper.testSort(integers0, new MergeSort())); System.out.println("歸併排序測試2資料量為10000"+SortTestHelper.testSort(integers1, new MergeSort())); System.out.println("歸併排序測試3資料量為100000"+SortTestHelper.testSort(integers2, new MergeSort())); }

執行結果

------------------------------隨機陣列--------------------------------
插入排序測試1資料量為100排序時長為: 0.001s
插入排序測試2資料量為10000排序時長為: 0.084s
插入排序測試3資料量為100000排序時長為: 5.868s
氣泡排序測試1資料量為100排序時長為: 0.0s
氣泡排序測試2資料量為10000排序時長為: 0.061s
氣泡排序測試3資料量為100000排序時長為: 9.069s
希爾排序測試1資料量為100排序時長為: 0.0s
希爾排序測試2資料量為10000排序時長為: 0.004s
希爾排序測試3資料量為100000排序時長為: 0.008s

程式碼優化

  • 雖然歸併排序是nlogn級別的演算法, 但是在陣列資料量比較小的時候, 插入排序的效率仍然是高於歸併排序的, 所以可以在對陣列分解到足夠小之後, 使用插入排序, 然後再遞迴進行歸併排序。
  • 如果一個數組是近乎有序的, 或者說是完全有序的, 上述步驟會有很多無用的merge操作, 所以可以在進行merge前增加一個判斷, 效率也會有一定的提高。

虛擬碼

package com.paal.demo.c01SortingBasic.optimize;

import com.paal.demo.Sort;
import com.paal.demo.c01SortingBasic.InsertionSort;

import java.util.Arrays;

/**
 * <p/>
 * <li>title: 歸併排序優化</li>
 * <li>@author: li.pan</li>
 * <li>Date: 2019/12/7 12:15 下午</li>
 * <li>Version: V1.0</li>
 * <li>Description:
 * 優化1: 對於arr[mid] <= arr[mid+1]的情況,不進行merge
 * 優化2: 對於小規模陣列, 使用插入排序
 * </li>
 */
public class MergeSortOptimize implements Sort {


    @Override
    public void sort(Integer[] arr) {
        int n = arr.length;
        sort(arr, 0, n - 1);
    }

    //遞迴使用歸併排序,對arr[l....r]的範圍進行進行排序
    private static void sort(Integer[] arr, int l, int r) {

        /**
         *  優化2: 對於小規模陣列, 使用插入排序
         */
        if( r - l <= 15 ){
            insertionSort(arr, l, r);
            return;
        }

        int mid = (l + r) / 2;
        sort(arr, l, mid);
        sort(arr, mid + 1, r);

        /**
         * 優化1: 對於arr[mid] <= arr[mid+1]的情況,不進行merge
         * 對於近乎有序的陣列非常有效,但是對於一般情況,有一定的效能損失
         */
        if (arr[mid] > arr[mid + 1])
            merge(arr, l, mid, r);

    }

    //將arr[l...mid]和arr[mid+1...r]兩部分進行歸併
    private static void merge(Integer[] arr, int l, int mid, int r) {

        // 開闢臨時空間,合併左半部分已經排好序的陣列和右半部分已經排好序的陣列
        Integer[] aux = Arrays.copyOfRange(arr, l, r + 1);

        // 初始化,i指向左半部分的起始位置索引;j指向右半部分起始索引位置mid+1
        int i = l, j = mid + 1;
        for (int k = l; k <= r; k++) {  // k指向兩個元素比較後歸併下一個需要放置的位置

            /**
             * 考慮陣列越界
             */
            if (i > mid) {  // 如果左半部分元素已經全部處理完畢,
                arr[k] = aux[j - l];
                j++;
            } else if (j > r) {   // 如果右半部分元素已經全部處理完畢
                arr[k] = aux[i - l];
                i++; // l表示偏移
            }
            /**
             * 真正比較
             */
            else if (aux[i - l] < aux[j - l]) {  // 左半部分所指元素 < 右半部分所指元素
                arr[k] = aux[i - l];
                i++;
            } else {  // 左半部分所指元素 >= 右半部分所指元素
                arr[k] = aux[j - l];
                j++;
            }
        }
    }

    // 對arr[l...r]的區間使用InsertionSort排序
    public static void insertionSort(Integer[] arr, int l, int r){

        for( int i = l + 1 ; i <= r ; i ++ ){
            Integer e = arr[i];
            int j = i;
            for( ; j > l && arr[j-1]>e ; j--)
                arr[j] = arr[j-1];
            arr[j] = e;
        }
    }

}

/**
     * 歸併排序優化測試
     */
    @Test
    public void mergeOptimizeSortTest() {
        Integer[] integers0 = SortTestHelper.generateRandomArray(100, 0, 1000000);
        Integer[] integers1 = SortTestHelper.generateRandomArray(10000, 0, 1000000);
        Integer[] integers2 = SortTestHelper.generateRandomArray(100000, 0, 1000000);
        System.out.println("------------------------------隨機陣列--------------------------------");
        System.out.println("插入排序測試1資料量為100" + SortTestHelper.testSort(integers0, new InsertionSort()));
        System.out.println("插入排序測試2資料量為10000" + SortTestHelper.testSort(integers1, new InsertionSort()));
        System.out.println("插入排序測試3資料量為100000" + SortTestHelper.testSort(integers2, new InsertionSort()));
        System.out.println("歸併排序測試1資料量為100" + SortTestHelper.testSort(integers0, new MergeSort()));
        System.out.println("歸併排序測試2資料量為10000" + SortTestHelper.testSort(integers1, new MergeSort()));
        System.out.println("歸併排序測試3資料量為100000" + SortTestHelper.testSort(integers2, new MergeSort()));
        System.out.println("歸併排序優化測試1資料量為100" + SortTestHelper.testSort(integers0, new MergeSortOptimize()));
        System.out.println("歸併排序優化測試2資料量為10000" + SortTestHelper.testSort(integers1, new MergeSortOptimize()));
        System.out.println("歸併排序優化測試3資料量為100000" + SortTestHelper.testSort(integers2, new MergeSortOptimize()));

        Integer[] integers00 = SortTestHelper.generateNearlyOrderedArray(100, 50);
        Integer[] integers11 = SortTestHelper.generateNearlyOrderedArray(10000, 5000);
        Integer[] integers22 = SortTestHelper.generateNearlyOrderedArray(100000, 50000);
        System.out.println("------------------------------近乎有序陣列--------------------------------");
        System.out.println("插入排序測試1資料量為100" + SortTestHelper.testSort(integers00, new InsertionSort()));
        System.out.println("插入排序測試2資料量為10000" + SortTestHelper.testSort(integers11, new InsertionSort()));
        System.out.println("插入排序測試3資料量為100000" + SortTestHelper.testSort(integers22, new InsertionSort()));
        System.out.println("歸併排序測試1資料量為100" + SortTestHelper.testSort(integers00, new MergeSort()));
        System.out.println("歸併排序測試2資料量為10000" + SortTestHelper.testSort(integers11, new MergeSort()));
        System.out.println("歸併排序測試3資料量為100000" + SortTestHelper.testSort(integers22, new MergeSort()));
        System.out.println("歸併排序優化測試1資料量為100" + SortTestHelper.testSort(integers00, new MergeSortOptimize()));
        System.out.println("歸併排序優化測試2資料量為10000" + SortTestHelper.testSort(integers11, new MergeSortOptimize()));
        System.out.println("歸併排序優化測試3資料量為100000" + SortTestHelper.testSort(integers22, new MergeSortOptimize()));
    }

執行結果

------------------------------隨機陣列--------------------------------
插入排序測試1資料量為100排序時長為: 0.001s
插入排序測試2資料量為10000排序時長為: 0.12s
插入排序測試3資料量為100000排序時長為: 8.578s
歸併排序測試1資料量為100排序時長為: 0.0s
歸併排序測試2資料量為10000排序時長為: 0.013s
歸併排序測試3資料量為100000排序時長為: 0.025s
歸併排序優化測試1資料量為100排序時長為: 0.0s
歸併排序優化測試2資料量為10000排序時長為: 0.001s
歸併排序優化測試3資料量為100000排序時長為: 0.003s
------------------------------近乎有序陣列--------------------------------
插入排序測試1資料量為100排序時長為: 0.0s
插入排序測試2資料量為10000排序時長為: 0.039s
插入排序測試3資料量為100000排序時長為: 2.764s
歸併排序測試1資料量為100排序時長為: 0.0s
歸併排序測試2資料量為10000排序時長為: 0.0s
歸併排序測試3資料量為100000排序時長為: 0.009s
歸併排序優化測試1資料量為100排序時長為: 0.0s
歸併排序優化測試2資料量為10000排序時長為: 0.0s
歸併排序優化測試3資料量為100000排序時長為: 0.0s