1. 程式人生 > >高階排序演算法

高階排序演算法

1 前言

對於基本排序演算法來說,時間複雜度一般都是O(n^2)。而高階排序演算法的時間複雜度一般都是O(nlogn)。高階排序演算法主要針對基本排序演算法進行優化。下面介紹幾種常見的高階排序演算法,希爾排序,歸併排序,快速排序,堆排序

2 希爾排序

希爾排序是插入排序的一個改進,它主要是用一個遞增序列來使陣列進行一個分組,然後對每組進行一個插入排序
例如遞增序列 h = 3*h + 1;那麼
先找到最大的遞增序列 h = n/3
接著我們就可以對
0 0+h 0+2*h …. 0 + k*h
1 1+h 1+2*h …. 1 + k*h
…..
i i+h i+2*h …
等進行插入排序,當h=1時,排序就完成了
相關演算法見下:

@Override
public void sort(Comparable[] array) {
    int length = array.length;
    int h = 0;

    /**
     * 遞增序列 3*k + 1
     */
    while (h < length / 3){
        h = 3 * h + 1;
    }
    //間隔為n的插入排序
    while (h >= 1){
        for (int i = h; i < length ; i++){
            for (int j = i ;j >= h ; j -= h){
                //將a[i] a[i-h] a[i -2h] 做插入排序
if (less(array[j],array[j-h])){ exch(array,j,j-h); } } } h = h/3; } }

可以看到,希爾排序就是對間隔為h數進行插入排序。

3 歸併排序

歸併排序採用了一種分治和遞迴思想,它是將一個數組分為兩部分,然後分別對兩部分進行排序,然後將兩個有序的陣列進行歸併成一個數組的過程。對兩個陣列的排序又可以採用遞迴的思想,分別進行拆分成兩個陣列,進行排序。然後再進行歸併。
歸併排序演算法如下:自頂向下

/**
 * @author Created by qiyei2015 on 2018/3/19.
 * @version: 1.0
 * @email: [email protected]
 * @description: 歸併排序
 */
public class MergeSort extends BaseSort{

    private Comparable[] aux;
    private static final int M = 15;

    @Override
    public void sort(Comparable[] array) {
        aux = new Comparable[array.length];

        int lo = 0;
        int hi = array.length - 1;
        sort(array,lo,hi);
    }

    /**
     * 歸併排序
     * @param array
     * @param lo
     * @param hi
     */
    private void sort(Comparable[]array,int lo,int hi){
        if (hi - lo <0){
            return;
        }
        int mid = lo + (hi - lo)/2;
        //排左半邊
        sort(array,lo,mid);
        //排右半邊
        sort(array,mid + 1,hi);
        //歸併
        merge(array,lo,mid,hi);
    }

    /**
     * 數組合並
     * @param array
     * @param lo
     * @param mid
     * @param hi
     */
    private void merge(Comparable[] array,int lo,int mid,int hi){
        int i = lo;
        int j = mid + 1;

        //將陣列array複製到aux中
        for (int k = lo ;k <= hi ; k++){
            aux[k] = array[k];
        }

        //aux[lo..mid] aux[mid+1..hi]
        for (int k = lo ; k <= hi ; k++){
            if (i > mid){
                //i 超過mid,說明左半邊用完,取右半邊
                array[k] = aux[j++];
            }else if (j > hi){
                //j 超過hi,說明右半邊用完,取左半邊
                array[k] = aux[i++];

            }else if (less(aux[i],aux[j])){
                //i 比j小,取i
                array[k] = aux[i++];
            }else {
                array[k] = aux[j++];
            }
        }
    }

}

可以看到,歸併排序的核心過程就是這個歸併的過程。其思想是用一個臨時陣列來存取元素。然後分別用兩個指標來計數兩個分段陣列。比較這兩個陣列,將較小的賦值到陣列中。這樣就完成了歸併過程

優化點:
1 在hi – lo <= M M為15等,也就是分割到一個大小差不多為16個數組的時候,可以考慮採用插入排序
2 在分割之後,如果本身陣列已經有序的情況,例如a[mid] <= mid[mid+1]時,就不用歸併了,因此優化如下:

* 歸併排序
 * @param array
 * @param lo
 * @param hi
 */
private void sort(Comparable[]array,int lo,int hi){
    if (hi - lo <= M){
        new InsertionSort().sort(array,lo,hi);
        return;
    }
    int mid = lo + (hi - lo)/2;
    //排左半邊
    sort(array,lo,mid);
    //排右半邊
    sort(array,mid + 1,hi);
    //歸併
    if (array[mid].compareTo(array[mid + 1]) > 0){
        merge(array,lo,mid,hi);
    }
}

對於連結串列等,可以考慮採用自底向上來進行歸併。

另外,堆排序是一種穩定的排序

4 快速排序

快速排序是一種非常經典和常用的排序,被譽為20世紀最偉大的排序。快速排序的思想和歸併類似,將陣列進行一個切分。這樣陣列就分為三部分了
a[0..v-1] a[v] a[v+1…]使其a[v]之前的數小於a[v] a[v]之後的數大於a[v]。然後對剩餘的數繼續進行快速排序,快速排序也用到了分治和遞迴的思想。
快速排序的實現如下:

/**
 * @author Created by qiyei2015 on 2018/3/25.
 * @version: 1.0
 * @email: [email protected]
 * @description:
 */
public class QuickSort extends BaseSort {
    @Override
    public void sort(Comparable[] array) {
        int lo = 0;
        int hi = array.length - 1;
        sort(array,lo,hi);
}

    /**
     * 快速排序,分治 遞迴
     * @param array
     * @param lo
     * @param hi
     */
    private void sort(Comparable[] array,int lo,int hi){
        //遞迴結束條件
        if (hi - lo <0){
            return;
        }
        //parttion 已經處於該位置上的有序了,因此該位置上的數不用排序
        int parttion = parttion(array,lo,hi);
        sort(array,lo,parttion -1);
        sort(array,parttion + 1,hi);
}

    /**
     * 找到切分點,將陣列分為 a[lo,j-1] a[j] a[j+1,hi]三部分,其中a[lo..j-1] < a[j],a[j+1..hi] > a[j]
     * @param array
     * @param lo
     * @param hi
     * @return
     */
    private int parttion(Comparable[] array, int lo, int hi){
        Comparable v = array[lo];
        int j = lo;
        //找到切分點 a[lo..j-1] < a[j],a[j+1..hi] > a[j]
        for (int i = lo + 1; i <= hi ;i++){
            //如果a[i]比v小,就交換j+1和i,並且j++
            if (less(array[i],v)){
                exch(array,j + 1,i);
                j++;
            }
        }
        exch(array,lo,j);
        return j;
    }

快速排序的關鍵在於切分,切分就是在v的位置a[v]已經排好序。切分思想如下:
以第一個元素a[lo] v為例,將該元素排好序,從左lo + 1到右掃描陣列,如果a[i]比v小,就交換a[i]與j+1。並且j++。這樣比v小的數就從lo + 1到j了。而大於等於v的數就排到a[j]及其後面去了。最後一個小於v的元素是a[j]然後將其與a[lo]交換,這樣a[lo…j-1]的元素就小於a[j]而其後的元素就大於等於a[j]了。

優化點:
1 切分以後的元素可以考慮採用插入排序
2 對於切分元素a[lo]進行隨機化。
3 對於有序的陣列可以採用雙路排序
4 對於大量範圍很小的資料採用三路快速排序
5 交換採用賦值操作,減少交換的次數

擴充套件
怎麼在N個數中找到第M個大小的數?
可以利用切分的思想。

5 堆排序

由於堆的性質,根節點的數總是大於(小於)子結點,我們以最大堆為例,堆排序就是我們將陣列從後往前,依次取堆的最大的元素(根節點),然後不斷的調整堆的結構。最後將陣列遍歷完畢時,也完成了排序。由於調整堆結構的複雜度為O(logn),因此堆排序的時間複雜度也是O(nlogn)。

/**
 * 堆排序
 * @param array
 */
@Override
public void sort(Comparable[] array) {
    MaxPQ<Integer> maxPQ = new MaxPQ(array.length);
    for (int i = 0 ; i < array.length ; i++){
        maxPQ.insert((Integer) array[i]);
    }
    for (int i = array.length - 1 ; i >= 0 ; i--){
        array[i] = maxPQ.delMax();
    }
}

其中MaxPQ是一個最大堆

以上就是一些高階排序演算法,除了希爾排序演算法的時間複雜度不好評估外(但是也小於O(n^2))。其他排序演算法的平均時間複雜度都是O(nlogn)