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

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

概念

快速排序是交換類排序,采用分治思想施戴,其基本原理是:通過一趟排序奖地,將待排序數(shù)組分割成獨(dú)立的兩部分间涵,其中一部分的關(guān)鍵字均比另一部分泻驮谩退疫;然后再分別對這兩部分序列遞歸進(jìn)行快速排序,從而使整個(gè)序列有序鸽素。

具體算法步驟:

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

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

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

Java實(shí)現(xiàn)

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

    /**
     * 交換數(shù)組arr中的第i個(gè)位置和第j個(gè)位置的關(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
  • 因?yàn)閘ow和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} 做同樣的操作即可完成整個(gè)排序戒努。

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

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

?
(1) 樞軸的選取方式的優(yōu)化
樞軸的選取方式有:(1) 固定位置選裙窗省;(2) 隨機(jī)位置選惹疟酢窝爪; (3) 三值取中法 等

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

隨機(jī)位置選炔甭伞:隨機(jī)選取當(dāng)前待排序序列的任意記錄作為樞軸。由于采取隨機(jī)腕侄,所以時(shí)間性能要強(qiáng)于固定位置選取小泉。

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

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

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

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

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

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

(2) 優(yōu)化小數(shù)組時(shí)的排序方案
當(dāng)局部排序數(shù)組長度較小時(shí)昆稿,采用插入排序纺座,而非快速排序,因?yàn)殚L度分割到夠小后溉潭,繼續(xù)分割的效率要低于直接插入排序净响。

(3) 略去不必要的交換
略去不必要的交換少欺,將交換操作改為替換操作。
因?yàn)榻粨Q操作需要進(jìn)行3次賦值操作馋贤,而替換操作只需要進(jìn)行1次賦值操作赞别。

Java實(shí)現(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ù)組時(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]變成中值;并返回計(jì)算的樞軸(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開始遍歷,因?yàn)橄聵?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)操作鱼蝉;

  • 此時(shí)待排序序列:
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
  • 因?yàn)閘ow和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} 做同樣的操作即可完成整個(gè)排序寸莫。

復(fù)雜度

時(shí)間復(fù)雜度:
時(shí)間復(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)系作者
  • 序言:七十年代末生逸,一起剝皮案震驚了整個(gè)濱河市敬特,隨后出現(xiàn)的幾起案子掰邢,更是在濱河造成了極大的恐慌牺陶,老刑警劉巖伟阔,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異掰伸,居然都是意外死亡皱炉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門狮鸭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來合搅,“玉大人,你說我怎么就攤上這事歧蕉≡植浚” “怎么了?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵惯退,是天一觀的道長赌髓。 經(jīng)常有香客問我,道長催跪,這世上最難降的妖魔是什么锁蠕? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮懊蒸,結(jié)果婚禮上荣倾,老公的妹妹穿的比我還像新娘。我一直安慰自己骑丸,他們只是感情好舌仍,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著通危,像睡著了一般铸豁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上黄鳍,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天推姻,我揣著相機(jī)與錄音,去河邊找鬼框沟。 笑死藏古,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的忍燥。 我是一名探鬼主播拧晕,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼梅垄!你這毒婦竟也來了厂捞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎靡馁,沒想到半個(gè)月后欲鹏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡臭墨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年赔嚎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胧弛。...
    茶點(diǎn)故事閱讀 38,094評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡尤误,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出结缚,到底是詐尸還是另有隱情损晤,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布红竭,位于F島的核電站尤勋,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏德崭。R本人自食惡果不足惜斥黑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望眉厨。 院中可真熱鬧锌奴,春花似錦、人聲如沸憾股。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽服球。三九已至茴恰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間斩熊,已是汗流浹背往枣。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留粉渠,地道東北人分冈。 一個(gè)月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像霸株,于是被迫代替她去往敵國和親雕沉。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評論 2 345

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

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