排序--快速排序及其優(yōu)化

版權(quán)聲明:本文源自簡書tianma,轉(zhuǎn)載請務(wù)必注明出處:http://www.lxweimin.com/p/df8a9e136295

概念

快速排序是交換類排序,采用分治思想,其基本原理是:通過一趟排序,將待排序數(shù)組分割成獨立的兩部分,其中一部分的關(guān)鍵字均比另一部分小;然后再分別對這兩部分序列遞歸進(jìn)行快速排序,從而使整個序列有序。

具體算法步驟:

  1. 在待排序的記錄序列中選取一個記錄作為樞軸(pivot);
  2. 通過一趟排序,將所有小于樞軸的記錄都移到樞軸的左邊,將所有大于樞軸的記錄都移到樞軸的右邊,其實就是將當(dāng)前待排序序列分為兩部分,左邊部分的記錄均小于右邊部分的記錄,這樣的操作叫做partition(分割),分割操作結(jié)束后,樞軸所處的位置就是最終排序后它所處的位置;
  3. 對樞軸左右兩邊的子序列重復(fù)步驟1和2,直至所有子記錄序列只剩下一個記錄為止。

以上步驟中,關(guān)鍵點是 1. 樞軸(pivot)的選取方式; 2. 對分割操作(partition)的細(xì)節(jié)處理。

未優(yōu)化的快速排序
  1. 樞軸的選取:將待排序序列的第1個記錄作為樞軸;
  2. 分割操作 : 分割操作中使用到了交換;

Java實現(xiàn)

// 定義接口
interface Sorter {
    /**
     * 將數(shù)組按升序排序
     */
    int[] sort(int[] arr);

    /**
     * 交換數(shù)組arr中的第i個位置和第j個位置的關(guān)鍵字
     */
    default void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

// 未優(yōu)化的快速排序
class QuickSorter implements Sorter {

    @Override
    public int[] sort(int[] arr) {
        quickSort(arr, 0, arr.length - 1);
        return arr;
    }

    /**
     * 對數(shù)組arr[low...high]的子序列作快速排序,使之有序
     */
    protected void quickSort(int[] arr, int low, int high) {
        int pivotLoc; // 記錄樞軸(pivot)所在位置
        if (low < high) {
            pivotLoc = partition(arr, low, high); // 將arr[low...high]一分為二,并返回樞軸位置

            quickSort(arr, low, pivotLoc - 1);// 遞歸遍歷arr[low...pivotLoc-1]
            quickSort(arr, pivotLoc + 1, high); // 遞歸遍歷arr[pivotLoc+1...high]
        }
    }

    /**
     * 在arr[low...high]選定pivot=arr[low]作為樞軸(中間位置),將arr[low...high]分成兩部分,
     * 前半部分的子序列的記錄均小于pivot,后半部分的記錄均大于pivot;最后返回pivot的位置
     */
    protected int partition(int[] arr, int low, int high) {
        int pivot;
        pivot = arr[low]; // 將arr[low]作為樞軸
        while (low < high) { // 從數(shù)組的兩端向中間掃描 // A
            while (low < high && arr[high] >= pivot) {  // B
                high--;
            }
            swap(arr, low, high); // 將比樞軸pivot小的元素交換到低位 // C
            while (low < high && arr[low] <= pivot) { //D
                low++;
            }
            swap(arr, low, high); // 將比樞軸pivot大的元素交換到高位 // E
        }
        return low; // 返回一趟下來后樞軸pivot所在的位置
    }
}

演示
為了方便演示,我對上面代碼中的分割操作partition方法的代碼進(jìn)行了標(biāo)注(分別標(biāo)注為 A,B,C,D,E)。
對于待排序序列 {5, 1, 9, 3, 7, 4, 8, 6, 2},我們來演示其第一趟排序過程:

low = 0, high = 8, pivot = arr[low] = 5;

  • A處,low = 0, high = 8, low<high,進(jìn)行A循環(huán);
  • B處,high的值不斷遞減,直至arr[high] = 2 小于pivot,跳出B循環(huán):
pivot
↓
5  1  9  3  7  4  8  6  2
↑                       ↑
low                    high
  • C處,執(zhí)行l(wèi)ow和high的元素交換:
                      pivot
                        ↓
2  1  9  3  7  4  8  6  5
↑                       ↑
low                    high
  • D處,low的值不斷遞增,直至arr[low] = 9 大于 pivot,跳出D循環(huán):
                      pivot
                        ↓
2  1  9  3  7  4  8  6  5
      ↑                 ↑
     low               high
  • E處,執(zhí)行l(wèi)ow和high的元素交換:
    pivot
      ↓
2  1  5  3  7  4  8  6  9
      ↑                 ↑
     low              high
  • A處,low =2, high = 8, low < high,繼續(xù)循環(huán)A;
  • B處,high的值不斷遞減,直至arr[high] = 4 小于pivot,跳出B循環(huán):
    pivot
      ↓
2  1  5  3  7  4  8  6  9
      ↑        ↑
     low      high
  • C處,執(zhí)行l(wèi)ow和high的元素交換:
             pivot
               ↓
2  1  4  3  7  5  8  6  9
      ↑        ↑
     low      high
  • D處,low的值不斷遞增,直至arr[low] = 7 大于 pivot,跳出D循環(huán):
             pivot
               ↓
2  1  4  3  7  5  8  6  9
            ↑  ↑
          low  high
  • E處,執(zhí)行l(wèi)ow和high的元素交換:
          pivot
            ↓
2  1  4  3  5  7  8  6  9
            ↑  ↑
          low  high
  • A處,low = 4, high = 5, low < high, 繼續(xù)循環(huán)A:
  • B處,high不斷遞減,直至high=4 等于 low,不滿足 low < high,跳出B循環(huán):
          pivot
            ↓
2  1  4  3  5  7  8  6  9
            ↑
           low  
           high
  • 因為low和high已經(jīng)重合,所以在接下來的C、D、E操作中序列均未發(fā)生變化
  • A處,low=4, high = 4, 不滿足 low < high, 跳出A循環(huán),最后返回low=4,即為pivot所在位置;

所以第1趟排序下來之后,序列會變成 {2, 1, 4, 3, 5, 7, 8, 6, 9};然后再對子序列{2, 1, 4, 3} 和 {7, 8, 6, 9} 做同樣的操作即可完成整個排序。

對于partition方法中的low和high,可以這樣理解:在low左邊的記錄都都小于等于樞軸pivot,在high右邊的記錄都大于等于樞軸pivot,那么當(dāng)low和high重合時,則表示已經(jīng)分割完畢,重合的位置(即low的值)就是樞軸pivot的位置。

快速排序的優(yōu)化

?
(1) 樞軸的選取方式的優(yōu)化
樞軸的選取方式有:(1) 固定位置選取;(2) 隨機位置選取; (3) 三值取中法 等

固定位置選取:選取當(dāng)前序列的第一個元素或者最后一個元素作為樞軸,上面的算法的樞軸選取方式即為固定位置選取。該方法不是一個好的選取方案,因為當(dāng)整個序列有序時,每次分割(partition)操作只會將待排序序列減1,此時為最壞情況,算法復(fù)雜度淪為O(n^2)。然而,在待排序的序列中局部有序是相當(dāng)常見的,所以固定位置選取樞軸不是一種好的選擇。

隨機位置選取:隨機選取當(dāng)前待排序序列的任意記錄作為樞軸。由于采取隨機,所以時間性能要強于固定位置選取。

三值取中法: 待排序序列的前(第一個位置)、中(中間位置)、后(最后一個位置)三個記錄中的中間值(按大小排序)作為樞軸,比如:

9 1 7 5 2 8 6 3 4
↑       ↑       ↑
low    mid    high
前      中      后

由于 9 > 4 > 2; 因此將4作為此次分割(partition)操作的樞軸。
三值取中操作后,整個序列變?yōu)椋?/p>

4 1 7 5 2 8 6 3 9
↑       ↑       ↑
low    mid    high
前      中      后

三值取中本質(zhì)上就是隨機位置選取,但是由于隨機位置選取過程中需要用到隨機種子來產(chǎn)生隨機數(shù),而三值取中不需要,所以三值取中要優(yōu)于隨機位置選取。

所以優(yōu)化樞軸的選取方式時,我們選擇三值取中的方式。

(2) 優(yōu)化小數(shù)組時的排序方案
當(dāng)局部排序數(shù)組長度較小時,采用插入排序,而非快速排序,因為長度分割到夠小后,繼續(xù)分割的效率要低于直接插入排序。

(3) 略去不必要的交換
略去不必要的交換,將交換操作改為替換操作。
因為交換操作需要進(jìn)行3次賦值操作,而替換操作只需要進(jìn)行1次賦值操作。

Java實現(xiàn)

// 優(yōu)化的快速排序
class OptimizedQuickSorter extends QuickSorter {

    /**
     * 插入排序最大數(shù)組長度值
     */
    private static final int MAX_LENGTH_INSERT_SORT = 7;

    /**
     * 對數(shù)組arr[low...high]的子序列作快速排序,使之有序
     */
    @Override
    protected void quickSort(int[] arr, int low, int high) {
        int pivotLoc; // 記錄樞軸(pivot)所在位置
        if ((high - low + 1) > MAX_LENGTH_INSERT_SORT) {
            // 待排序數(shù)組長度大于臨界值,則進(jìn)行快速排序
            pivotLoc = partition(arr, low, high); // 將arr[low...high]一分為二,并返回樞軸位置

            quickSort(arr, low, pivotLoc - 1);// 遞歸遍歷arr[low...pivotLoc-1]
            quickSort(arr, pivotLoc + 1, high); // 遞歸遍歷arr[pivotLoc+1...high]
        } else {
            // 2. 優(yōu)化小數(shù)組時的排序方案,將快速排序改為插入排序
            insertSort(arr, low, high); // 對arr[low...high]子序列進(jìn)行插入排序
        }
    }

    /**
     * 在arr[low...high]中利用三值取中選取樞軸(pivot),將arr[low...high]分成兩部分,
     * 前半部分的子序列的記錄均小于pivot,后半部分的記錄均大于pivot;最后返回pivot的位置
     */
    @Override
    protected int partition(int[] arr, int low, int high) {
        int pivot;
        pivot = medianOfThree(arr, low, high); // 1. 優(yōu)化排序基準(zhǔn),使用三值取中獲取中值
        while (low < high) { // 從數(shù)組的兩端向中間掃描 // A
            while (low < high && arr[high] >= pivot) { // B
                high--;
            }
            // swap(arr, low, high); // 將比樞軸pivot小的元素交換到低位
            arr[low] = arr[high]; // 3. 優(yōu)化不必要的交換,使用替換而不是交換  // C
            while (low < high && arr[low] <= pivot) { // D
                low++;
            }
            // swap(arr, low, high); // 將比樞軸pivot大的元素交換到高位
            arr[high] = arr[low]; // 3. 優(yōu)化不必要的交換,使用替換而不是交換 // E
        }
        arr[low] = pivot; // F
        return low; // 返回一趟下來后樞軸pivot所在的位置
    }

    /**
     * 通過三值取中(從arr[low...high]子序列中)獲取樞軸pivot的值,讓arr[low]變成中值;并返回計算的樞軸(pivot)
     */
    private int medianOfThree(int[] arr, int low, int high) {
        int mid = low + ((high - low) >> 1); // mid = low + (high-low)/2, 中間元素下標(biāo)

        // 使用三值取中得到樞軸
        if (arr[low] > arr[high]) { // 目的:讓arr[low] <= arr[high]
            swap(arr, low, high);
        }
        if (arr[mid] > arr[high]) { // 目的:讓arr[mid] <= arr[high]
            swap(arr, mid, high);
        }
        if (arr[mid] > arr[low]) { // 目的: 讓arr[low] >= arr[mid]
            swap(arr, low, mid);
        }
        // 經(jīng)過上述變化,最終 arr[mid]<=arr[low]<=arr[high],則arr[low]為中間值
        return arr[low];
    }

    /**
     * 對子序列arr[low...high]進(jìn)行插入排序
     */
    private void insertSort(int[] arr, int low, int high) {
        int i, j;
        int tmp;
        for (i = low + 1; i <= high; i++) { // 從下標(biāo)low+1開始遍歷,因為下標(biāo)為low的已經(jīng)排好序
            if (arr[i] < arr[i - 1]) {
                // 如果當(dāng)前下標(biāo)對應(yīng)的記錄小于前一位記錄,則需要插入,否則不需要插入,直接將記錄數(shù)增加1
                tmp = arr[i]; // 記錄下標(biāo)i對應(yīng)的元素
                for (j = i - 1; j >= low && arr[j] > tmp; j--) {
                    arr[j + 1] = arr[j]; // 記錄后移
                }
                arr[j + 1] = tmp; // 插入正確位置
            }
        }
    }
}

演示
為了方便演示,我對上面代碼中的分割操作partition方法的代碼仍然進(jìn)行了標(biāo)注(分別標(biāo)注為 A,B,C,D,E,F)。
對于待排序序列 {5, 1, 9, 3, 7, 4, 8, 6, 2},我們來演示其第一趟排序過程:

low = 0, high = 8, high-low+1=9 > MAX_LENGTH_INSERT_SORT, 所以需要進(jìn)行快速排序,接下來進(jìn)行分割(partition)操作;

  • 此時待排序序列:
5  1  9  3  7  4  8  6  2
↑                       ↑
low                    high
  • 三值取中前:
5  1  9  3  7  4  8  6  2
↑           ↑           ↑
low        mid        high

三值取中后:

pivot
↓
5  1  9  3  2  4  8  6  7
↑           ↑           ↑
low        mid        high

pivot = 5;

  • A處,low = 0, high = 8, low < high, 進(jìn)行A循環(huán);
  • B處,high的值不斷遞減,直至arr[high] = 4 小于pivot,跳出B循環(huán):
5  1  9  3  2  4  8  6  7
↑              ↑
low           high
  • C處,arr[low] = arr[high],將低位的值替換成高位的值:
4  1  9  3  2  4  8  6  7
↑              ↑
low           high
  • D處,low的值不斷遞增,直至arr[low] = 9 大于 pivot,跳出D循環(huán):
4  1  9  3  2  4  8  6  7
      ↑        ↑
     low     high
  • E處,arr[high] = arr[low], 將高位的值替換成低位的值:
4  1  9  3  2  9  8  6  7
      ↑        ↑
     low     high
  • A處,low = 2, high = 5, low < high, 進(jìn)行A循環(huán);
  • B處,high的值不斷遞減,直至arr[high] = 2 小于pivot,跳出B循環(huán):
4  1  9  3  2  9  8  6  7
      ↑     ↑
     low   high
  • C處,arr[low] = arr[high],將低位的值替換成高位的值:
4  1  2  3  2  9  8  6  7
      ↑     ↑
     low   high
  • D處,low的值不斷遞增,直至low = 4, high = 4, low == high,不滿足 low<high,跳出D循環(huán):
4  1  2  3  2  9  8  6  7
            ↑
           low
           high
  • 因為low和high已經(jīng)重合,所以在接下來的E操作中序列未發(fā)生變化;
  • A處,low=4, high = 4, 不滿足 low < high, 跳出A循環(huán);
  • F處, arr[low] = pivot:
4  1  2  3  5  9  8  6  7
            ↑
           low
           high
  • 最后返回low = 4,即為pivot所在的位置。

所以這趟排序下來之后,序列會變成 {4 1 2 3 5 9 8 6 7};然后再對子序列{4, 1, 2, 3} 和 {9, 8, 6, 7} 做同樣的操作即可完成整個排序。

復(fù)雜度

時間復(fù)雜度:
時間復(fù)雜度為O(nlogn),在對快速排序進(jìn)行各種細(xì)節(jié)性的優(yōu)化后,快速排序的性能大大提高,在一般條件下超越了其它排序方法,故得此名。

空間復(fù)雜度:
就空間復(fù)雜度來說,主要是遞歸造成的棧空間的使用,最好情況,遞歸的深度為log2n,其空間復(fù)雜度也就為O(logn),最壞情況,需要進(jìn)行n‐1遞歸調(diào)用,其空間復(fù)雜度為O(n),平均情況,空間復(fù)雜度也為O(logn)。
?
參考鏈接:
常見排序算法 - 快速排序 (Quick Sort)
三種快速排序以及快速排序的優(yōu)化

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,772評論 3 422
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,947評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,960評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,350評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,549評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,104評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,914評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,089評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,340評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,834評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,106評論 2 375

推薦閱讀更多精彩內(nèi)容

  • 某次二面時,面試官問起Js排序問題,吾絞盡腦汁回答了幾種,深感算法有很大的問題,所以總計一下! 排序算法說明 (1...
    流浪的先知閱讀 1,202評論 0 4
  • //聯(lián)系人:石虎QQ: 1224614774昵稱:嗡嘛呢叭咪哄 //常用的排序算法 細(xì)節(jié)請看:http://blo...
    石虎132閱讀 421評論 0 4
  • 概述 排序有內(nèi)部排序和外部排序,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進(jìn)行排序,而外部排序是因排序的數(shù)據(jù)很大,一次不能容納全部...
    蟻前閱讀 5,210評論 0 52
  • 概述:排序有內(nèi)部排序和外部排序,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進(jìn)行排序,而外部排序是因排序的數(shù)據(jù)很大,一次不能容納全部...
    每天刷兩次牙閱讀 3,740評論 0 15
  • 自初中起,我就有了男神。 我是一個思想發(fā)育比較早熟的姑娘。那時所在的班級,是一個有六七十學(xué)生的大班,其中不乏優(yōu)秀的...
    蘇涼涼涼閱讀 324評論 0 0