java泛型中使用的排序算法——歸並排序及分析


一、引言

我們知道,java中泛型排序使用歸並排序或TimSort。歸並排序以O(NlogN)最壞時間運行,下面我們分析歸並排序過程及分析證明時間復雜度;也會簡述為什么java選擇歸並排序作為泛型的排序算法。

二、圖解歸並排序過程

  1. 算法思想:采用分治法:
  • 分割:遞歸地把當前序列平均分割成兩半。
  • 集成:在保持元素順序的同時將上一步得到的子序列集成到一起(歸並)。
  • 歸並操作:指的是將兩個已經排序的序列合並成一個序列的操作。歸並排序算法依賴歸並操作。
  1. 歸並過程:取兩個輸入數組A、B和一個輸出數組C以及3個索引index1,index2,index3分別指向三個數組開始端。並在A[index1]、B[index2]中較小者拷貝到數組C中的下一個位置,相關的索引+1。當A、B中有一個數組走完時,將另一個數組中的元素全部拷貝到數組C中。

假設輸入數組A[1、7、9、13],B[5、8、15、17],算法過程如下。1與5比較,1存入數組C中;接下來7與5比較,5存入C中。

接着7與8比較,7存入C中;9與8比較,8存入C中。

接着這樣的過程進行比較,直到13與15比較,13存入C中。

這時候A已經用完,將B中剩余的元素全部拷貝到C中即可。

從圖中可以看出,合並兩個排序表是線性的,最多進行N-1次比較(可以改變輸入序列,使得每次只有一個數進入數組C,除了最后一次,最后一次至少有兩個元素進入C)。對於歸並排序,N=1的時候,排序結果是顯然的;否則,遞歸將前半部分與后半部分分別歸並排序。這是使用分治的思想。

三、java實現歸並排序

public class MergeSort {
    public static void main(String[] args) {
        Integer[] integers = {711391558,17};
        System.out.println("原序列:" + Arrays.toString(integers));
        mergeSort(integers);
        System.out.println("排序后:" + Arrays.toString(integers));
    }

    public static <T extends Comparable<? super T>> void mergeSort(T[] a) {
        //因為merge操作是最后一行,所以任何時候只需要一個臨時數組
        T[] tmpArray = (T[]) new Comparable[a.length];
        mergeSort(a, tmpArray, 0, a.length - 1);
    }

    private static <T extends Comparable<? super T>> void mergeSort(T[] a, T[] tmpArray, int left, int right) {
        if (left < right) {
            int center = (left + right) / 2;
            mergeSort(a, tmpArray, left, center);
            mergeSort(a, tmpArray, center + 1, right);
            merge(a, tmpArray, left, center + 1, right);
        }
    }

    /**
     * 合並左右數據方法
     *
     * @param a               :原數組
     * @param tmpArray        : 臨時數組
     * @param leftPos         :左邊開始下標
     * @param rightPos:右邊開始下標
     * @param rightEnd:右邊結束下標
     * @param <T>:元素泛型
     */

    private static <T extends Comparable<? super T>> void merge(T[] a, T[] tmpArray, int leftPos, int rightPos, int rightEnd) {
        int leftEnd = rightPos - 1;
        int tmpPos = leftPos;
        int numElements = rightEnd - leftPos + 1;
        //合並操作
        while (leftPos <= leftEnd && rightPos <= rightEnd) {
            if (a[leftPos].compareTo(a[rightPos]) <= 0) {
                tmpArray[tmpPos++] = a[leftPos++];
            } else {
                tmpArray[tmpPos++] = a[rightPos++];
            }
        }
        // 復制前半部分
        while (leftPos <= leftEnd) {
            tmpArray[tmpPos++] = a[leftPos++];
        }
        //復制后半部分
        while (rightPos <= rightEnd) {
            tmpArray[tmpPos++] = a[rightPos++];
        }
        // 回寫原數組
        for (int i = 0; i < numElements; i++, rightEnd--) {
            a[rightEnd] = tmpArray[rightEnd];
        }
    }
}

四、歸並排序分析

我們假設N是2的冪,我們遞歸總可以均分為兩部分。對於N=1,歸並排序所用時間是常數,即O(1)。對於N個數歸並排序的用時等於兩部分時間之和再加上合並的時間。合並是線性的,因為最多使用N-1次比較。推導如下:

上面我們假設了N=2k,對於N不是2的冪(通常都是這種情況),其實,結果都是差不多的,也只是最多再多一次過程而已。

  1. 時間復雜度:從分析可以看出,歸並排序的最好最壞都穩定在O(NlogN)
  2. 空間復雜度:需要O(N)個臨時空間進行合並操作。
  3. 穩定性:穩定。采用分治的思想,每次合並時,在前面的總會先存入臨時數組內。

五、簡談java泛型為什么選擇歸並排序

歸並排序與其他O(NlogN)排序算法相比,時間很依賴比較算法與在數組中移動元素(包括臨時數組中)的相對開銷。這是與語言環境有關的。對於Java來說,進行比較可能比較耗時(使用Comparator);但移動元素屬於引用賦值,不是龐大對象的拷貝。歸並算法在所有流行的使用比較的算法中使用最少的比較次數。另一個原因是歸並排序是穩定的,這在某些特殊的場景特別重要。

六、總結

本篇通過畫圖詳述了歸並排序的過程,還通過數學證明歸並排序時間復雜度穩定在O(NlogN);簡談了下java選擇歸並排序的原因。java中對於基本類型的排序,使用的是快速排序。


免責聲明!

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



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