06-快速排序(Quick Sort)

快速排序(Quick Sort)

看到名字,就知道這種排序算法速度非常快。那到底有多快呢?在前面冒泡排序時(shí),就有提到過這種排序算法,它的平均時(shí)間復(fù)雜度為O(nlogn),但看到其最壞時(shí)間復(fù)雜度為O(n^2),不過,雖然有最壞的情況,但是還是有辦法降低最壞情況出現(xiàn)的概率,所以總體來講,效率還是非常高的。但是在前面也介紹過,堆排序與歸并排序,其時(shí)間復(fù)雜度都是O(nlogn)級(jí)別的,但是這三個(gè)O(nlogn)級(jí)別的排序算法,哪個(gè)算法會(huì)更快呢?其實(shí)快速排序會(huì)更加快,也就是說,在本章節(jié)介紹的快速排序會(huì)比對(duì)排序與歸并排序速度更快。

快速排序簡(jiǎn)介

快速排序是1960年由查爾斯·安東尼·理查德·霍爾(Charles Antony Richard Hoare),一般稱為東尼·霍爾(Tony Hoare)

其實(shí)結(jié)合前面的歸并排序介紹,可以發(fā)現(xiàn)一個(gè)特點(diǎn),現(xiàn)在的排序算法都是幾十年前的結(jié)論,也就是說,幾十年過去了,到現(xiàn)在還沒有一個(gè)穩(wěn)定的,被大家公認(rèn)的排序算法來替代這些優(yōu)秀的算法,還望大家來突破這些算法。

快速排序執(zhí)行流程
  1. 從序列中選擇一個(gè)軸點(diǎn)元素(pivot)

    • 假設(shè)每次選擇0位置的元素為軸點(diǎn)元素,例如下圖的序列

      現(xiàn)在選擇0位置的元素6作為軸點(diǎn)元素,一旦定義好了軸點(diǎn)元素后,就開始執(zhí)行步驟2

  2. 利用軸點(diǎn)元素將序列分割成2個(gè)子序列

    • 將小于軸點(diǎn)元素的元素放在軸點(diǎn)元素的前面(左側(cè))
    • 將大于軸點(diǎn)元素的元素放在軸點(diǎn)元素的后面(右側(cè))
    • 等于軸點(diǎn)元素的元素放任意一邊都可以

    所以利用上圖的序列,進(jìn)行分割以后,就會(huì)變?yōu)橄聢D的這樣一個(gè)序列,通過這樣分割,相對(duì)于原來的序列,會(huì)變得更加有序一點(diǎn)

  3. 對(duì)子序列進(jìn)行1,2操作

    • 知道不能再分割(子序列中只剩下一個(gè)元素)

    所以對(duì)上面的序列再次執(zhí)行1,2操作的話,最終得到的結(jié)果如下圖

    再次執(zhí)行1,2操作,得到下圖的結(jié)果

最終,將所有元素一次排好序。

所以可以發(fā)現(xiàn),快速排序的本質(zhì)是

逐漸將每一個(gè)元素都轉(zhuǎn)換成軸點(diǎn)元素,當(dāng)所有元素都變?yōu)檩S點(diǎn)元素時(shí),就拍好序了

那這個(gè)流程應(yīng)該怎樣去實(shí)現(xiàn)呢?

構(gòu)造軸點(diǎn)

假設(shè)現(xiàn)在有下列的序列,這個(gè)序列有點(diǎn)特殊,其中有兩個(gè)6,兩個(gè)8,為了表示區(qū)分,方別用6a/6b,8a/8b來表示,其中begin指向首元素,end指向末尾元素(這里的end和前面章節(jié)的end指向的位置有一點(diǎn)區(qū)別)

現(xiàn)在希望將6a變?yōu)檩S點(diǎn),一般會(huì)先將該元素進(jìn)行備份,利用一個(gè)臨時(shí)的變量,將該值保存起來。

由于現(xiàn)在是begin有空的位置,所以從end到begin+1開始掃描元素,在本序列中,首先掃描到的是7,由于7大于軸點(diǎn)元素,所以只需要將end--即可,繼續(xù)掃描

現(xiàn)在掃描到的是5,由于5是小于軸點(diǎn)元素,所以應(yīng)該放到序列的左邊。然后begin++

現(xiàn)在空的位置變?yōu)閑nd方向,所以掃描的防線變?yōu)閺腷egin到end的方向。掃描到的第一個(gè)元素是8a,由于8a是比軸點(diǎn)元素大的,所以將8a的值直接覆蓋end指向的位置,然后將end--

由于現(xiàn)在空位置是由begin直接指向的,所以本次掃描方向由end方向到begin方向,繼續(xù)掃描,這次掃描發(fā)現(xiàn)元素為9,比軸點(diǎn)元素大,所以只需要將end--即可

由于空位置沒有發(fā)生變化,所以繼續(xù)從end掃描到begin,本次掃描到的元素為4,其值比軸點(diǎn)元素小,所以將元素4直接覆蓋掉begin指向的位置,然后begin++

現(xiàn)在空位置是右end指針指向,所以掃描方向發(fā)生變化,變?yōu)閺腷egin掃描到end,由于本次掃描到的元素為8b,值比軸點(diǎn)元素大,所以將8b的值覆蓋掉end指針指向的位置,然后進(jìn)行end--操作

完成后,空的位置現(xiàn)在有begin指針指向,所以掃描方向變?yōu)閑nd到begin方向,本次掃描到的元素是6b,是等于軸點(diǎn)元素的,由于等于軸點(diǎn)元素的元素,放軸點(diǎn)元素任意一邊都可以,所以直接end--就行,不過這里執(zhí)行的操作是將相等的元素放到另外一邊,所以這里是將end蚊子相等的元素放在begin,begin++,最終的效果如下圖

現(xiàn)在空的位置由end指向,所以掃描方向由begin到end方向。不過這次掃描發(fā)現(xiàn)值比軸點(diǎn)元素小,所以直接begin++就可以了,begin++后得到的begin等于end,一旦發(fā)現(xiàn)begin和end重疊,就意味著軸點(diǎn)元素構(gòu)建完畢

由于之前備份了6a的值,所以現(xiàn)在要做的事情是將6a的值,覆蓋當(dāng)前空出來的位置,這樣6a就歸位了

所以最終發(fā)現(xiàn),6a變?yōu)檩S點(diǎn)后,最終找到了自己的位置,并且軸點(diǎn)構(gòu)建完畢。

軸點(diǎn)構(gòu)建總結(jié):掃描方向主要看空出來的位置是由begin指向還是有end指向

  • 如果end指向空出來的元素, 則從begin掃描到end
  • 如果是begin指向空出來的元素,則從end掃描到begin

根據(jù)上面的分析,最終可以得到獲取軸點(diǎn)的實(shí)現(xiàn)代碼如下

/*
* 構(gòu)造出[begin,end)范圍內(nèi)的軸點(diǎn)元素
* @return 軸點(diǎn)元素的最終位置
* */
private int pivotIndex(int begin, int end) {
    //備份軸點(diǎn)元素
    E pivot = array[begin];
    //end指向最后一個(gè)元素
    end--;
    while (begin < end) {
        while (begin < end) {
            if (cmp(pivot,array[end]) < 0) {//右邊元素大于軸點(diǎn)元素
                end--;
            } else  {//右邊元素小于等于軸點(diǎn)元素
                array[begin++] = array[end];
                break;
            }
        }
        while (begin < end) {
            if (cmp(pivot,array[begin]) >0) {//左邊元素小于軸點(diǎn)
                begin++;
            } else {//左邊元素大于等于軸點(diǎn)元素
                array[end--] = array[begin];
                break;
            }
        }
    }
    //將軸點(diǎn)元素放入最終的位置
    array[begin] = pivot;
    //返回軸點(diǎn)元素的位置
    return begin;
}

然后利用獲取到的軸點(diǎn),對(duì)begin到end范圍內(nèi)的元素進(jìn)行快速排序的代碼

/*
* 對(duì)[begin,end)范圍內(nèi)的元素進(jìn)行快速排序
* */
private void quickSort(int begin, int end) {
    if (end - begin < 2) return;
    //確定軸點(diǎn)位置
    int mid = pivotIndex(begin,end);
    //對(duì)子序列也進(jìn)行快速排序
    quickSort(begin,mid);//左邊子序列快速排序
    quickSort(mid + 1 ,end);//右邊子序列快速排序
}

利用上面代碼,結(jié)合前面的幾種排序算法,對(duì)20000條數(shù)據(jù)進(jìn)行排序的結(jié)果如下圖

可以看到,快速排序與前面幾種排序結(jié)果排序后,得到的結(jié)果非常優(yōu)秀。如果再將數(shù)據(jù)進(jìn)行增加到50000條數(shù)據(jù),得到的結(jié)果就更加明顯了,結(jié)果如下(這里的排序,對(duì)前面性能較差的幾種排序就不再做對(duì)比了)

時(shí)間復(fù)雜度分析

在軸點(diǎn)左右元素?cái)?shù)量比較均勻的情況下,是快速排序性能最佳的時(shí)候。

在這個(gè)時(shí)候,可以得到所消耗時(shí)間的表達(dá)式為:T(n) = 2 * T(n/2) + O(n)

可以看到,這個(gè)表達(dá)式與歸并排序的表達(dá)式是一樣的,所以在最好的情況下, 快速排序的時(shí)間復(fù)雜度與歸并排序的時(shí)間復(fù)雜度相同,都是O(nlogn)

如果軸點(diǎn)左右兩邊元素?cái)?shù)量及其不均勻,則是時(shí)間復(fù)雜度最壞的情況。

例如現(xiàn)在有如下的序列

將元素7作為軸點(diǎn),軸點(diǎn)構(gòu)造完成后的結(jié)果為

并往復(fù)執(zhí)行構(gòu)造軸點(diǎn),最終得到的每一步結(jié)果為

所以在這個(gè)時(shí)候,就是最壞時(shí)間復(fù)雜度的時(shí)候。在這種情況下,所消耗時(shí)間的表達(dá)式為:T(n) = T(n-1) + O(n) = O(n^2);

避免最壞情況

為了降低最壞情況的出現(xiàn)概率,一般采取的做法是

  1. 隨機(jī)選擇軸點(diǎn)元素

所以優(yōu)化后的代碼為

private int pivotIndex(int begin, int end) {
    //隨機(jī)選擇一個(gè)元素跟begin位置進(jìn)行交換
    swap(begin,(int)(Math.random() * (end - begin)) + begin);
    //備份軸點(diǎn)元素
    E pivot = array[begin];
    //end指向最后一個(gè)元素
    end--;
    while (begin < end) {
        while (begin < end) {
            if (cmp(pivot,array[end]) < 0) {//右邊元素大于軸點(diǎn)元素
                end--;
            } else  {//右邊元素小于等于軸點(diǎn)元素
                array[begin++] = array[end];
                break;
            }
        }
        while (begin < end) {
            if (cmp(pivot,array[begin]) >0) {//左邊元素小于軸點(diǎn)
                begin++;
            } else {//左邊元素大于等于軸點(diǎn)元素
                array[end--] = array[begin];
                break;
            }
        }
    }
    //將軸點(diǎn)元素放入最終的位置
    array[begin] = pivot;
    //返回軸點(diǎn)元素的位置
    return begin;
}

快速排序時(shí)間復(fù)雜度總結(jié):

最好,平均時(shí)間復(fù)雜度為:O(nlogn)

最壞時(shí)間復(fù)雜度:O(n^2)

空間復(fù)雜度:O(logn)

與軸點(diǎn)相等的元素

在前面,如果遇到軸點(diǎn)元素相等的元素,都是直接將該元素放到軸點(diǎn)元素的另一邊。具體是怎么做的呢?請(qǐng)看下圖,現(xiàn)在所有元素都是相等的

  1. 備份軸點(diǎn)元素


  2. 執(zhí)行第一次確定元素位置,發(fā)現(xiàn)值是相等的,這時(shí),原來軸點(diǎn)右邊的元素,被放到了軸點(diǎn)的左邊


  3. 執(zhí)行第二次確定元素位置,發(fā)現(xiàn)值是相等的,這時(shí),將原來軸點(diǎn)左邊的元素,被放到軸點(diǎn)的右邊


  4. 執(zhí)行第三次確定元素位置,發(fā)現(xiàn)值是相等的,這時(shí),原來軸點(diǎn)右邊的元素,被放到了軸點(diǎn)的左邊


  5. 執(zhí)行第四次確定元素位置,發(fā)現(xiàn)值是相等的,這時(shí),將原來軸點(diǎn)左邊的元素,被放到軸點(diǎn)的右邊


  6. 將備份的元素放到begin與end重合的位置


發(fā)現(xiàn),如果用原來的這種確定軸點(diǎn)的方式,有一個(gè)好處就是,軸點(diǎn)確定以后,依然可以平均分割原來的序列。

所以在pivot與end元素進(jìn)行比較時(shí),不是≤或者≥的原因是為了提高性能,避免出現(xiàn)最壞時(shí)間復(fù)雜度的情況。

如果將cmp位置的判斷分別改為≤,≥會(huì)起到什么效果呢?

這樣進(jìn)行判斷,會(huì)導(dǎo)致軸點(diǎn)元素切割出來的序列,非常不均勻,可能會(huì)導(dǎo)致最壞時(shí)間復(fù)雜度O(n^2)

為了進(jìn)行對(duì)比,現(xiàn)生成5萬條相等的數(shù)據(jù),利用原來的算法進(jìn)行測(cè)試,得到的結(jié)果如下圖

現(xiàn)在將判斷條件分別改為≤,≥,得到的結(jié)果如下圖

最終控制臺(tái)打印出了非常多的這種信息,從提示信息可以看出,是發(fā)生了棧溢出,也就是說??臻g消耗完了,原因是現(xiàn)在的軸點(diǎn)切割非常不均勻,每次切割只會(huì)減少一個(gè)數(shù)據(jù)規(guī)模,也就意味著quickSort函數(shù)會(huì)遞歸調(diào)用n次,在當(dāng)前程序中,是要遞歸調(diào)用5萬次,需要開辟5萬次??臻g,最終導(dǎo)致??臻g不夠用。

所以為了對(duì)比出最終的效果,現(xiàn)在將數(shù)據(jù)規(guī)模調(diào)整到1萬條。在不加=的情況下,得到的結(jié)果為

加上=后的結(jié)果為

可以看到,最終的比較結(jié)果,差距非常大。所以在進(jìn)行比較判斷的時(shí)候,不要使用≥或者≤

demo下載地址

完!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 概述:JDK提供了概述:JDK提供了對(duì)于數(shù)組排序的庫函數(shù),java.util.Arrays類中的一些列重載的sor...
    張晨輝Allen閱讀 3,190評(píng)論 1 5
  • 總結(jié)一下常見的排序算法。 排序分內(nèi)排序和外排序。內(nèi)排序:指在排序期間數(shù)據(jù)對(duì)象全部存放在內(nèi)存的排序。外排序:指在排序...
    jiangliang閱讀 1,532評(píng)論 0 1
  • 原文地址 快速排序 原理 快速排序是C.R.A.Hoare提出的一種交換排序。它采用分治的策略,所以也稱其為分治排...
    gyl_coder閱讀 1,007評(píng)論 0 0
  • 一、直接插入排序 直接插入排序(Insertion Sort)的基本思想是:每次將一個(gè)待排序的元素記錄,按其關(guān)鍵字...
    kevin16929閱讀 659評(píng)論 0 0
  • 排序算法 1.什么叫排序? 排序無處不在 十大排序算法: 2 算法介紹 什么是排序算法的穩(wěn)定性? 回答:如果相等的...
    愛飛的揚(yáng)閱讀 457評(píng)論 0 0

友情鏈接更多精彩內(nèi)容