快速排序

快速排序

快速排序的基本流程

快速排序顧名思義是通過實踐發現,性能快於其他排序算法的一種算法。他的基本排序思想分爲下面幾步,假設給定一個待排序的數組arr

  1. 如果arr中元素的個數是0或者1,則結束程序,因爲快速排序是用遞歸實現的,所以這一步可以看做遞歸的出口
  2. 選取數組arr中任一個元素,成爲樞紐元(pivot)
  3. 通過pivot將數組分成兩個不想交的子數組arr1和arr2,其中arr1中的元素均小於pivot,arr2中的元素均大於pivot
  4. 在對上面的arr1和arr2遞歸的實現這個過程,直到排序完成
    快速排序基本流程

選取樞紐元pivot的幾種方案

選取第一個元素

在排序序列是隨機輸入的情況下,選取第一個元素並沒有什麼影響,但是如果出現待排序序列是基本有序的或者逆序的,那麼就會出現所有的元素不是被劃分到arr1就是arr2中,且在以後的每一個遞歸過程中,都無法避免這種情況出現,導致快速排序的性能退化成N2級。但是其實代碼卻並沒有做什麼實際的工作。所以這種方法是不可取的。

隨機選取

採用隨機選取的方法可以有效避免上面的情況,但是隨機數的生成也是一個開銷較大的工作,不利於提高快速排序的效率。

三數中值法

假設如果每次我們選取的樞紐元都是這個數組的中值,那麼一半元素分到arr1,一半元素分到arr2,再好不過,但是要每次計算出來這個中值,又得花費一些時間,拖慢排序的效率。因此我們的前輩們爲我們提出了這樣一種方案,就是選取待排序數組的左端元素,右端元素和中間元素,取他們中間的中值。那麼就上面那個數組**[8, 1, 4, 9, 6, 3, 5, 2, 7, 0]**,arr[left] = 8, arr[right] = 0, arr[(left + right) / 2] = 6,所以pivot = 6。實踐證明這是最好的安排。

分割策略

關於快速排序的第三步分割策略也有好多種,今天我們介紹教科書上介紹的被認爲是比較優秀的一種分割策略。基本分爲以下幾個步驟:

  1. 交換樞紐元與最後的元素,是樞紐元離開要被分割的數據段
  2. i從第一個元素開始,j從第二個元素開始,向中間移動。當i找到一個比pivot大的元素停下,當j找到一個比pivot小的元素停下,交換位置,直到i和j錯開
  3. 第二步以後,在交換i位置的元素和最後一個元素,分割結束,這個時候位置i的左側都是小於pivot的元素,右側都是大於pivot的元素
  4. 這個過程是可以優化的,在上一節三數中值法取到arr[left]、arr[right]、pivot後,其實可以將最大值放到最右側,最小值放到最左側,pivot放到right-1的位置,然後i從left+1開始,j從right-2開始,
    分割策略

進一步優化

對於數據量小的序列,因爲快速排序表現是不如插入排序表現出色的,所以我們這裏取一個界定值CUTOFF,當待排序序列元素個數小於10的時候,使用插入排序,大於10的那時候使用快速排序。

結合以上討論,我們給出代碼實現:

    // 當序列元素個數少於這個界限則使用快速排序
    // 小規模數據用插入排序效率更高
    private static final int CUTOFF = 10;

    /**
     * 快速排序
     *
     * @param arr 待排序的數組
     * @param left 數組最左邊的元素索引
     * @param right 數組最右邊的元素索引
     */
    public static void quickSort(int[] arr, int left, int right) {
        if (left + CUTOFF < right) {
            int pivot = media3(arr, left, right);
            int i = left, j = right - 1;
            for ( ; ;) {
                while (arr[++i] < pivot) {}
                while (arr[--j] > pivot) {}
                if (i < j) {
                    swapReferences(arr, i, j);
                } else {
                    break;
                }
            }
            swapReferences(arr, i , right - 1);
            quickSort(arr, left, i - 1);
            quickSort(arr, i + 1, right);
        } else {
            insertSort(arr, left, right);
        }

    }
	
	// 插入排序
    private static void insertSort(int[] arr, int left, int right) {
        int j = 0;
        for (int i = left + 1; i <= right; i++) {
            int tmp = arr[i];
            for (j = i; j > 0 && tmp < arr[j - 1]; j--) {
                arr[j] = arr[j - 1];
            }
            arr[j] = tmp;
        }
    }

    /**
     * 三數中值法選定樞紐元,取left,right, (left + right) / 2三數的中間值,並將三數中最小的放到arr[left]
     * 最大的放到arr[right],樞紐元放到arr[right - 1]
     *
     * @param arr 待排序數組
     * @param left 數組的第一位元素索引
     * @param right 數組的最後一位元素索引
     * @return 樞紐元 pivot
     */
    private static int media3(int[] arr, int left, int right) {
        int center = (left + right) / 2;
        if (arr[center] < arr[left]) {
            swapReferences(arr, left, center);
        }
        if (arr[right] < arr[left]) {
            swapReferences(arr, left, right);
        }
        if (arr[right] < arr[center]) {
            swapReferences(arr, center, right);
        }
        swapReferences(arr, center, right - 1);
        return arr[right - 1];
    }

    private static void swapReferences(int[] arr, int left, int right) {
        int tmp = arr[left];
        arr[left] = arr[right];
        arr[right] = tmp;
    }

快速排序的時間複雜度

時間複雜度這個問題說起來比較複雜,這裏只給出書本上的結論吧,有時間在研究研究推導公式

最壞情況(每次選取的pivot都是最小值):O(N2)

平均情況:O(NlogN)

git源碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章