高級排序算法之歸併排序,快速排序

前言

承接上文基礎排序算法—冒泡,插入,選擇,相比之下,歸併排序和快速排序更爲高效,時間複雜度均爲O(nlogn),相比簡單排序的O(n^2)好了不少,下面介紹一下這兩種算法的思路,實現和主要指標.主要思路來自<數據結構與算法之美>java

正文

歸併排序

主要思路

在歸併排序採用分冶的思想,使用遞歸實現.描述以下git

  1. 開始歸併排序
  2. 若是數組元素小於兩個,無需排序,結束
  3. 不然須要排序,歸併排序數組左側,歸併排序數組右側,按序合併左右側

採用的是自頂至下的思路,例如[1,5,3,7,4,6],左側爲[1,5,3],右側爲[7,4,6],左側歸併排序後爲[1,3,5],右側歸併排序後爲[4,6,7],按序合併後[1,3,4,5,6,7]這裏的關鍵是只在主流程思考,不要試圖代入到遞歸的子流程中,引用一句: "編寫遞歸代碼的關鍵是,只要遇到遞歸,咱們就把它抽象成一個遞推公式,不用想一層層的調用關係,不要試圖用人腦去分解遞歸的每一個步驟" ,遞歸公式以下github

merge_sort(arr[low,high))=merge_sort(arr[low,mid))+merge_sort(arr[mid,high)
merge(arr,low,mid,high)

java實現以下算法

public void sort(Comparable[] arr) {
        if (arr.length <= 1) {
            return;
        }
        mergeSort(arr, 0, arr.length);
    }


    private void mergeSort(Comparable[] arr, int low, int high) {
        if (high - low <= 1) {
            return;
        }
        int mid = low + (high - low) / 2;
        mergeSort(arr, low, mid);
        mergeSort(arr, mid, high);
        merge(arr, low, mid, high);
    }

    private void merge(Comparable[] arr, int low, int mid, int high) {
        Comparable[] left = Arrays.copyOfRange(arr, low, mid);
        Comparable[] right = Arrays.copyOfRange(arr, mid, high);
        int l = 0, r = 0;
        int pos = low;
        for (int i = 0; i < (high - low); i++) {
            Comparable next = null;
            if (l == left.length) {
                next = right[r++];
            } else if (r == right.length) {
                next = left[l++];
            } else if (left[l].compareTo(right[r]) <= 0) {
                next = left[l++];
            } else {
                next = right[r++];
            }
            arr[pos++] = next;
        }
    }

merge方法將兩個有序數組合併爲一個有序數組,最後的判斷邏輯有一點繁瑣,王爭文中有提到能夠用哨兵簡化,也許是我使用方法不對,使用哨兵後感受反而更麻煩了,最終採用了繁瑣的寫法.數組

時間複雜度

對於遞歸實現,分析時間複雜度時也要推導出公式,對左右數組歸併排序數量級都是一半,所以爲T(n/2),merge()方法是對兩個數組按序排列,時間複雜度爲O(n), 當n=1時,時間複雜度爲常數,由此可得數據結構

T(n) = T(n/2) + T(n/2) + n = 2*T(n/2) + n
T(1) = C

下面對公式進行代入ui

T(n) = 2*T(n/2) + n
		 = 2*(2*T(n/4) + n/2) + n
		 = 2*(2*(2*T(n/8)+n/4) + n/2) + n
		 = 2^3*T(n/2^3) + 3n
		 = 2^k*T(n/2^k) + kn
當T(n/2^k) == T(1) 即n/2^k == 1 可得k=logn(底數爲2),代入到T(n)中得

T(n) = n*C +logn*n

因此歸併排序的時間複雜度爲O(nlogn),根據歸併排序的思路,它的算法複雜度不受數組順序影響.code

是原地排序嗎?

不是,merge過程當中須要對左右子數組複製進行歸併,儘管遞歸過程當中會不斷申請額外空間,可是同一時間申請的最大額外空間爲O(n),空間複雜度爲O(n)排序

穩定嗎?

歸併排序數組元素的移動只發生在merge階段,當左右相等時,咱們會優先選擇左側(left[l].compareTo(right[r]) <= 0),所以是穩定的遞歸

快速排序

主要思路

快排經過partition(劃分)將數組劃分爲中間值的左側和右側.保證左側<=中間值<右側,再對左側右側(不包含中間值所在下標)遞歸處理,實現排序.

快排的遞歸僞代碼以下

quick_sort(arr,low,high)={
  if (high-low<=1) return
	mid = partition(arr,low,high)
	quick_sort(arr,low,mid)
	quick_sort(arr,mid+1,high)
}

java代碼以下

public void sort(Comparable[] arr) {
        if (arr.length <= 1) {
            return;
        }
        quickSort(arr, 0, arr.length);
    }

    private void quickSort(Comparable[] arr, int low, int high) {
        if (high - low <= 1) {
            return;
        }
        int mid = partition(arr, low, high);
        quickSort(arr, low, mid);
        quickSort(arr, mid + 1, high);
    }

    private int partition(Comparable[] arr, int low, int high) {
        Comparable cmpValue = arr[high - 1];
        int i = low;
        for (int j = low; j < high - 1; j++) {
            if (arr[j].compareTo(cmpValue) <= 0) {
                swap(arr, i++, j);
            }
        }
        swap(arr, i, high - 1);
        return i;
    }

快速排序的核心在於partition(劃分),這裏有兩種方式:

  1. 以arr[0]爲中間值,左右各選一個錨點,將左側>中間值的與右側<=中間值的作交換,直到左右錨點相遇/左右錨點反序/任意錨點到達邊界,這樣左側都小於等於中間值,右側都大於中間值
  2. 相似於選擇排序,將數組在邏輯上劃分爲左側的已排序部分和右側的未排序部分,初始時已排序部分爲空.依次將小於中間值的數字插入到已排序部分

時間複雜度

和歸併排序相似,時間複雜度的遞推公式爲

T(n) = n + T(n/2) + T(n/2)  = n + 2*T(n/2) 
T(1) = C

推到過程同上,時間複雜度也是O(nlogn)

是原地排序嗎?

是的,快速排序過程當中不佔用額外空間

穩定嗎?

不穩定,從上文基礎排序算法—冒泡,插入,選擇中咱們得知選擇排序是不穩定的,快速排序的劃分過程採用的事選擇排序的思想,所以也是不穩定的

歸併排序 vs 快速排序

  • 兩種排序的時間複雜度相同,O(nlogn),都很優秀
  • 歸併排序空間複雜度爲O(n),快速排序的空間複雜度爲O(1)
  • 歸併排序是穩定的,快速排序是不穩定的
  • 兩種排序的使用都用了遞歸
  • 遞歸的使用方法上,歸併排序是自底至上,先劃分執行子任務,子任務完成後,再歸併.快速排序是自頂至下,先進行partition,再劃分執行子任務.

從實際使用來講,快速排序的效率更高,通常的庫實現也會優先選擇它.


源碼地址github

相關文章
相關標籤/搜索