快速排序及其優化

1、引言

顧名思義,快速排序是實踐中的一種快速排序算法,在C++或對Java基礎類型的排序中特別有用。它的平均運行時間是O(NlogN);但最壞情形性能爲O(N2)。我會先介紹快速排序過程,再討論如何優化。java

2、快速排序(quicksort)

  • 算法思想

採用分治法,將數組分爲兩部分,並遞歸調用。將數組S排序的快排過程算法

  1. 若是S中元素個數是0或1,則直接返回;
  2. 取S中任一元素v,稱之爲樞紐元(pivot);【樞紐元的選取策略很重要,下面會詳述
  3. 將S-{v}(S中除了樞紐元中的其他元素)劃分爲兩個不相交的集合S1和S2,S1集合中的全部元素小於等於樞紐元v,S2中的全部元素大於等於樞紐元;
  4. 返回quicksort(S1),樞紐元v,quicksort(S1</sub2)。
  • 樞紐元的選取策略
  1. 取第一個或者最後一個:簡單但很傻的選擇(啊,9龍,上面這圖???)。當輸入序列是升序或者降序時,這時候就會致使S1集合爲空,除樞紐元外全部元素在S2集合,這種作法,最壞時間複雜度爲O(N2)。
  2. 隨機選擇:這是比較安全的作法。除非隨機數發生器出現錯誤,而且連續產生劣質分割的機率比較低。但隨機數生成開銷較大,這樣就增長了運行時間。
  3. 三數中值分割法:一組序列的中值(中位數)是樞紐元最好的選擇(由於能夠將序列均分爲兩個子序列,歸併排序告訴咱們,這時候是O(NlogN);但要計算一組數組的中位數就比較耗時,會減慢快排的效率。但能夠經過計算數組的第一個,中間位置,最後一個元素的中值來代替。好比序列:[8,1,4,9,6,3,5,2,7,0]。第一個元素是8,中間(left+right)/2(向下取整)元素爲6,最後一個元素爲0。因此中位數是6,即樞紐元是6。顯然使用三數分割法消除了預排序輸入的壞情形,而且實際減小了14%的比較。
  • 快排過程
  1. 將樞紐元與數組最後一個元素調換,使樞紐元離開要被分割的數據段;數組

  2. 初始化兩個索引left和right,分別指向數組第一個與倒數第二個元素;安全

  3. 若是left索引指向的元素小於樞紐元,則left++;不然,left中止。right索引指向的元素大於樞紐元,right--;不然,right中止。markdown

  4. 若是left<right,則交換兩個元素,循環繼續3,4步驟;不然跳出循環,將left對應的元素與樞紐元交換(這時候完成了分割)。遞歸調用這兩個子序列。app

    假設全部元素互異(即都不相等)。下面會說重複元素怎麼處理。post

    接下來要作的就是將小於樞紐元的元素移到數組左邊,大於樞紐元的元素移到數組右邊。性能

    當left在right的左邊時,咱們將left右移,移過那些小於樞紐元的元素,並將right左移,移過那些大於樞紐元的元素。當left和right中止時,left指向一個大於樞紐元的元素,right指向一個小於樞紐元的元素,若是left<right,則將這兩個元素交換。這樣是將一個大於樞紐元的元素推向右邊而把小於樞紐元的元素推向左邊。咱們來圖示過程:left不動,而right左移一個位置,以下圖:優化

    咱們交換left與right指向的元素,重複這個過程,直到left>right。ui

    至此,咱們能夠看到,left左邊的元素都小於樞紐元,右邊的元素都大於樞紐元。咱們繼續遞歸左右序列,最終可完成排序。

    上面咱們假設的是元素互異,下面咱們討論重複元素的處理狀況。

  • 重複元素的處理:簡單說是遇到與樞紐元相等的元素時,左右索引須要中止嗎?
  1. 若是隻有其中一箇中止:這包含兩種,若是隻中止左、或者右索引,這將致使等於樞紐元的元素都移動到一個集合中。考慮序列全部元素都是重複元素,會是最壞情形O(N2)。
  2. 若是都不中止:這須要防範左右索引越界,而且不用交換元素。但正如上面圖示的正確過程是,樞紐元須要與left索引指向的元素進行交換。仍是考慮全部元素相同的狀況,這會致使序列全分到左邊,這樣仍是最壞情形O(N2)
  3. 都中止:仍是考慮元素全都相等的狀況,這樣看似會進行不少次「無心義」的交換;但正面的效果倒是,left與right交錯是發生在中間位置,這時恰好將序列均分爲兩個子序列,仍是歸併排序的原理,這是O(NlogN)。咱們分析指出,只有這種狀況能夠避免二次方。

在大規模輸入量中,重複元素仍是挺多的。考慮能將這些重複元素進行有效排序,仍是很重要。

快速排序真的快嗎?其實也不必定,對於小數組(N<=20)的輸入序列,快速排序不如插入排序而且在咱們上面的優化中,採用三數中值分割時,遞歸獲得的結果能夠是隻有一個,或者兩個元素,這時會有錯誤。因此,繼續優化是將小的序列用插入排序代替,這會減小大約15%的運行時間。較好的截止範圍是10(其實5-20產生的效果差很少)。

對於三數中值分割還能夠進行優化:假設輸入序列爲a,則選擇a[left],a[center],a[right],選擇出樞紐值,並將最小,與最大值分別放到a[left],a[right],將樞紐值放到a[right-1]處,這樣放置也是正確的位置,而且能夠防止right向右進行比較時不會越界;這樣左右起始位置就是left+1,right-2。

3、優化彙總的java實現快速排序:

public class Quicksort {
    /**
     * 截止範圍
     */

    private static final int CUTOFF = 10;

    public static void main(String[] args) {
        Integer[] a = {8149635270};
        System.out.println("快速排序前:" + Arrays.toString(a));
        quicksort(a);
        System.out.println("快速排序後:" + Arrays.toString(a));
    }

    public static <T extends Comparable<? super T>> void quicksort(T[] a) {
        quicksort(a, 0, a.length - 1);
    }

    private static <T extends Comparable<? super T>> void quicksort(T[] a, int left, int right) {
        if (left + CUTOFF <= right) {
            //三數中值分割法獲取樞紐元
            T pivot = median3(a, left, right);

            // 開始分割序列
            int i = left, j = right - 1;
            for (; ; ) {
                while (a[++i].compareTo(pivot) < 0) {
                }
                while (a[--j].compareTo(pivot) > 0) {
                }
                if (i < j) {
                    swapReferences(a, i, j);
                } else {
                    break;
                }
            }
            //將樞紐元與位置i的元素交換位置
            swapReferences(a, i, right - 1);
            //排序小於樞紐元的序列
            quicksort(a, left, i - 1);
            //排序大於樞紐元的序列
            quicksort(a, i + 1, right);
        } else {
            //插入排序
            insertionSort(a, left, right);
        }
    }

    private static <T extends Comparable<? super T>> median3(T[] a, int left, int right) {
        int center = (left + right) / 2;
        if (a[center].compareTo(a[left]) < 0) {

            swapReferences(a, left, center);
        }
        if (a[right].compareTo(a[left]) < 0) {
            swapReferences(a, left, right);
        }
        if (a[right].compareTo(a[center]) < 0) {
            swapReferences(a, center, right);
        }
        // 將樞紐元放置到right-1位置
        swapReferences(a, center, right - 1);
        return a[right - 1];
    }

    public static <T> void swapReferences(T[] a, int index1, int index2) {
        T tmp = a[index1];
        a[index1] = a[index2];
        a[index2] = tmp;
    }

    private static <T extends Comparable<? super T>> void insertionSort(T[] a, int left, int right) {
        for (int p = left + 1; p <= right; p++) {
            T tmp = a[p];
            int j;

            for (j = p; j > left && tmp.compareTo(a[j - 1]) < 0; j--) {
                a[j] = a[j - 1];
            }

            a[j] = tmp;
        }
    }

}
//輸出結果
//快速排序前:[8, 1, 4, 9, 6, 3, 5, 2, 7, 0]
//快速排序後:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
複製代碼

4、快速排序分析

  1. 最壞時間複雜度:即元素都分到一個子序列,另外一個子序列爲空的狀況,時間複雜度爲O(N2)。
  2. 最好時間複雜度:即序列是均分爲兩個子序列,時間複雜度是O(NlogN),分析與歸併排序差很少。
  3. 平均時間複雜度:O(NlogN)
  4. 空間複雜度:O(logN)

5、總結

本篇從如何較好選擇樞紐元,分析重複元素的處理及遞歸分紅小數組時更換爲插入排序三個方面進行快速排序的優化,系統全面詳述了快速排序原理、過程及其優化。快速排序以平均時間O(NlogN)進行,是java中基礎類型使用的排序算法。能夠去看一下Arrays.sort方法。到這裏,我就要回過頭去完善求解topK問題了,能夠利用快速排序的思想,達到平均O(N)求解topK。

以爲能夠的小夥伴們點個推薦或小贊支持啊。

相關文章
相關標籤/搜索