天天看點

快速排序

文章目錄

    • @[toc]

#算法思想

通過一趟快速排序将待排序的記錄分割成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分的記錄的關鍵字小,則可分别對這兩部分記錄繼續進行排序,達到整個記錄有序。

實作快速排序算法的核心是partition函數,這個函數的主要目的先選取當中的一個關鍵字(稱為樞軸),然後盡可能将他放在一個位置,使得它左邊的值都比它小,右邊的值比它大。

#實作

快速排序算法實作

public class QuickSort3 {

    public void quickSort(int[] a){
        qSort(a,0,a.length - 1);
        //列印輸出
        for (int i = 0; i < a.length; i++) {
            System.out.println(a[i]);
        }
    }

    private void qSort(int[] a, int low, int high) {
        int pivot = 0;
        if(low < high){
            //将數組a一分為二
            pivot = partition(a,low,high);
            //對第一部分進行遞歸排序
            qSort(a,low,pivot);
            //對第二部分進行遞歸排序
            qSort(a,pivot + 1,high);
        }
    }

    private int partition(int[] a, int low, int high) {
        //用第一個元素作為樞軸記錄
        int pivotkey = a[low];
        while(low < high){
            //将比樞軸記錄小的交換到低端
            while(low < high && a[high] >= pivotkey){
                high--;
            }
            swap(a,low,high);
            //将比樞軸記錄大的交換到高端
            while(low < high && a[low] <= pivotkey){
                low++;
            }
            swap(a,low,high);
        }
        //傳回樞軸所在的位置
        return low;
    }

    private void swap(int[] a, int low, int high) {
        int temp = a[low];
        a[low] = a[high];
        a[high] = temp;
    }

    public static void main(String[] args) {
        new QuickSort3().quickSort(new int[]{50,10,90,30,70,40,80,60,20});
    }
}
           

快速排序算法的最優時間複雜度是O(nlogn),在如下的情況下是最優的:對于一組具有n個關鍵字的記錄,partition函數每次都劃分得很均勻,也就是每次調用partition函數之後,比樞軸小的關鍵字和比樞軸大的關鍵字的數量基本相等。這樣下次調用partition函數的時候所用的時間是上次調用partition函數的兩倍加上比較元素的時間,是以這是最優的情況。

在最壞的情況指的就是每次調用partition函數,隻得到一個比上一次劃分少一個記錄的子序列(因為隻要調用partition函數,必須至少會有一次關鍵字的交換)。由于隻減少了一個關鍵字,是以後面還需要進行n-1次(總共有n個關鍵字)遞歸調用partition函數。是以到最後總共需要比較的次數是∑n−1i=1=n−1+n−2+…+1=n(n−1)2,是以最終的時間複雜度是O(n2)。

在平均的情況下,樞軸的關鍵字應該在k位置(1≤k≤n)。是以最優情況下的時間複雜度是O(nlogn)。

那麼快速排序算法的空間複雜度又是如何呢?最優和平均情況下空間複雜度都是O(logn),在最壞情況就是n−1次調用,是以空間複雜度是O(n)。

#快速排序算法的優化

##優化一:優化樞軸的選取位置

可以發現我們上面的算法中,樞軸的選取是在第一個位置的,這種選取樞軸位置的最大弊端就是很可能在調用partition函數的時候,隻更換了兩個元素的位置,而其他位置的元素并沒有發生變化,這就是上面分析中提到的最壞的情況,發現導緻這種情況的根源在于選取的樞軸的大小要麼太大要麼太小(因為太大或者太小,high或low的值隻能減小一個位置或者增加一個位置),那麼優化的思路就是讓樞軸既不會太大又不會太小。是以可以采用三數取中法:這種方法就是取三個關鍵字先進行排序,将中間的數作為樞軸,一般是取左端、右端和中間的三個數。這樣排序之後就能基本保證樞軸既不會太大又不會太小。是以我們隻需要修改partition函數如下:

private int partition2(int[] a, int low, int high) {
        //用第一個元素作為樞軸記錄
        int pivotkey = 0;
        //計算數組中間的下标
        int m = low + (high - low)/2;
        if(a[low] > a[high]){
            swap(a, low, high);
        }
        if(a[m] > a[high]){
            swap(a, m, high);
        }
        if(a[low] > a[m]){
            swap(a, low, m);
        }
        pivotkey = a[low];
        while(low < high){
            //将比樞軸記錄小的交換到低端
            while(low < high && a[high] >= pivotkey){
                high--;
            }
            swap(a,low,high);
            //将比樞軸記錄大的交換到高端
            while(low < high && a[low] <= pivotkey){
                low++;
            }
            swap(a,low,high);
        }
        //傳回樞軸所在的位置
        return low;
    }
           

##優化二:優化不必要的交換

經過上面的優化,我們選取樞軸關鍵字基本是适中大小的,分析樞軸的位置變化過程,從最後一個位置到第一個位置,再到中間那個位置。其實樞軸的目标就是中間那個位置,是以在樞軸到達中間位置的很多交換其實是不必要的。基于這個思路可以繼續優化partition函數如下:

private int partition3(int[] a, int low, int high) {
        //用第一個元素作為樞軸記錄
        int pivotkey = 0;
        //計算數組中間的下标
        int m = low + (high - low)/2;
        if(a[low] > a[high]){
            swap(a, low, high);
        }
        if(a[m] > a[high]){
            swap(a, m, high);
        }
        if(a[low] > a[m]){
            swap(a, low, m);
        }
        pivotkey = a[low];
        //把樞軸元素備份到一個臨時變量中
        int temp = pivotkey;
        while(low < high){
            //将比樞軸記錄小的交換到低端
            while(low < high && a[high] >= pivotkey){
                high--;
            }
            //采用替換而不是交換的方式操作
            a[low] = a[high];
            //将比樞軸記錄大的交換到高端
            while(low < high && a[low] <= pivotkey){
                low++;
            }
            //采用替換而不是交換的方式操作
            a[high] = a[low];
        }
        //将樞軸的值替換回給a[low]
        a[low] = temp;
        //傳回樞軸所在的位置
        return low;
    }
           

##優化三:優化資料量較小時的排序

在資料量較小的時候使用簡單排序算法反而更簡單,更高效,正如排序數組{2,1,3}的時候,使用簡單排序算法比如直接插入排序隻要兩次比較久搞定了,使用快速排序的話還要劃分,明顯效率低一些。是以我們可以對partition函數進行小數組的優化,有資料認為當數組的長度不大于7的時候算是小數組,也有認為是50,這個其實不是最重要的,這個時候就使用直接插入排序算法就很好用。是以基于這個思路優化代碼如下:

private void qSort(int[] a, int low, int high) {
        int pivot = 0;
        if(low < high && (high - low) > MAX_LENGTH_SORT){
            //MAX_LENGTH_SORT暫且定為50
            //将數組a一分為二
            pivot = partition3(a,low,high);
            //對第一部分進行遞歸排序
            qSort(a,low,pivot);
            //對第二部分進行遞歸排序
            qSort(a,pivot + 1,high);
        }else{
            //小數組的時候使用直接插入排序
            insertSort(a);
        }
    }
           
private void qSort2(int[] a, int low, int high) {
        int pivot = 0;
        if(low < high && (high - low) > MAX_LENGTH_SORT){
            while(low < high){
                //将數組a一分為二
                pivot = partition3(a,low,high);
                //對第一部分進行遞歸排序
                qSort2(a,low,pivot);
                //對第二部分進行尾遞歸,這裡之是以可以将pivot+1賦給low,是因為一/次遞歸結束
                //之後,low的值就沒有用處了。下一次遞歸調用的執行的就是qSort(a,pivot + 1,high)
                low = pivot +1;
            }
        }else{
            //小數組的時候使用直接插入排序
            insertSort(a);
        }
    }
           
上一篇: 快速排序
下一篇: 快速排序