06-快速排序(Quick Sort)

快速排序(Quick Sort)

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

快速排序簡介

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

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

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

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

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

  2. 利用軸點元素將序列分割成2個子序列

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

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

  3. 對子序列進行1,2操作

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

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

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

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

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

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

那這個流程應該怎樣去實現(xiàn)呢?

構(gòu)造軸點

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

時間復雜度分析

在軸點左右元素數(shù)量比較均勻的情況下,是快速排序性能最佳的時候。

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

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

如果軸點左右兩邊元素數(shù)量及其不均勻,則是時間復雜度最壞的情況。

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

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

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

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

避免最壞情況

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

  1. 隨機選擇軸點元素

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

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

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

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

最壞時間復雜度:O(n^2)

空間復雜度:O(logn)

與軸點相等的元素

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

  1. 備份軸點元素


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


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


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


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


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


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

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

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

這樣進行判斷,會導致軸點元素切割出來的序列,非常不均勻,可能會導致最壞時間復雜度O(n^2)

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

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

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

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

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

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

demo下載地址

完!

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

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

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