算法筆記-排序02:歸并排序,快速排序

思路

歸并排序的思想是先將數(shù)組分散為小數(shù)組分別排序,然后將結(jié)果歸并起來(lái)。

原地歸并的抽象方法

將兩個(gè)已經(jīng)排序好的數(shù)組歸并為一個(gè)數(shù)組這一操作對(duì)于歸并排序的意義不言而喻,以下是歸并方法的實(shí)現(xiàn):

public static void merge(Comparable[] a, int low, int mid, int high){
    int i = low, j = mid+1;
    
    for (int k = low; k <= high; k++)
        indexA[k] = a[k];
    
    for (int k = low; k <= high; k++){
        if (i > mid) a[k] = indexA[j++];
        else if (j > high) a[k] = indexA[i++];
        else if (less(indexA[j], indexA[i])) a[k] = indexA[j++];
        else a[k] = indexA[i++];
    }
}
自頂向下的歸并排序

基于原地歸并的抽象方法實(shí)現(xiàn)了另一種遞歸歸并,這是應(yīng)用高效算法設(shè)計(jì)中分治思想的典型例子:

public class Merge {
    
    private static Comparable[] indexA;
    
    public static void merge(Comparable[] a, int low, int mid, int high){
        
        int i = low, j = mid+1;
        
        for (int k = low; k <= high; k++)
            indexA[k] = a[k];
        
        for (int k = low; k <= high; k++){
            if (i > mid) a[k] = indexA[j++];
            else if (j > high) a[k] = indexA[i++];
            else if (less(indexA[j], indexA[i])) a[k] = indexA[j++];
            else a[k] = indexA[i++];
        }
    }
    
    public static void sort(Comparable[] a, int low, int high){
        if (indexA==null) indexA = new Comparable[a.length];
        if (high <= low) return;
        int mid = low + (high-low)/2;
        sort(a, low, mid);
        sort(a, mid+1, high);
        merge(a, low, mid, high);
    }
  
    
    private static boolean less(Comparable v, Comparable w){
        return v.compareTo(w) < 0;
    }
    
    private static void exchange(Comparable[] a, int i, int j){
        Comparable t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
    
    private static void show(Comparable[] a){
        for (int i = 0; i < a.length; i++)
            System.out.print(a[i]);
        System.out.println();
    }
      
    public static boolean isSorted(Comparable[] a){
        for (int i = 1; i < a.length; i++){
            if (less(a[i],a[i-1])) return false;
        }
        return true;
    }
}

在實(shí)際運(yùn)用中,希爾排序和歸并排序的運(yùn)行時(shí)間之差在常數(shù)范圍內(nèi)

運(yùn)行時(shí)間
自底向上的歸并排序

實(shí)現(xiàn)歸并排序的另一種方式是從小數(shù)組開(kāi)始?xì)w并:首先我們將數(shù)組的每一個(gè)元素都當(dāng)做一個(gè)只有一個(gè)元素的數(shù)組,然后將其兩兩歸并。然后我們將整個(gè)數(shù)組的每?jī)蓚€(gè)元素都當(dāng)做一個(gè)小數(shù)組,然后將其兩兩歸并,然后四個(gè)四個(gè)歸并,依次類推,直到最后歸并成一個(gè)大數(shù)組,排序就完成了。
完整實(shí)現(xiàn)代碼如下:

public class MergeBU {
    private static Comparable[] indexA;
    
    public static void merge(Comparable[] a, int low, int mid, int high){
        
        int i = low, j = mid+1;
        
        for (int k = low; k <= high; k++)
            indexA[k] = a[k];
        
        for (int k = low; k <= high; k++){
            if (i > mid) a[k] = indexA[j++];
            else if (j > high) a[k] = indexA[i++];
            else if (less(indexA[j], indexA[i])) a[k] = indexA[j++];
            else a[k] = indexA[i++];
        }
    }
    
    public static void sort(Comparable[] a){
        if (indexA == null) indexA = new Comparable[a.length];
        for (int sz = 1; sz<a.length; sz = sz+sz)
            for (int low = 0; low < a.length-sz; low += sz+sz)
                merge(a,low,low+sz-1,Math.min(low+sz+sz-1, a.length-1));
    }
    
    public static void main(String[] args){
        Integer[] a = {9,8,7,6,5,4,3,2,1};
        sort(a);
        for (Integer i: a){
            System.out.println(i);
        }
    }
    
    private static boolean less(Comparable v, Comparable w){
        return v.compareTo(w) < 0;
    }
    
    private static void exchange(Comparable[] a, int i, int j){
        Comparable t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
    
    private static void show(Comparable[] a){
        for (int i = 0; i < a.length; i++)
            System.out.print(a[i]);
        System.out.println();
    }
    
    public static boolean isSorted(Comparable[] a){
        for (int i = 1; i < a.length; i++){
            if (less(a[i],a[i-1])) return false;
        }
        return true;
    }
}

兩種實(shí)現(xiàn)方式的速度比較(給一千個(gè)大小為一萬(wàn)的數(shù)組排序):

運(yùn)行結(jié)果
快速排序

快速排序可能是應(yīng)用的最為廣泛的一種算法,它流行的原因是實(shí)現(xiàn)簡(jiǎn)單,適用于各種不同的輸入數(shù)據(jù)且在一般的應(yīng)用中比其他排序算法都要快的多。快速排序的優(yōu)點(diǎn):
是原地排序(只需要一個(gè)很小的輔助棧)。
所需時(shí)間跟NlgN成正比。

快速排序思路:

快速排序和歸并排序是互補(bǔ)的,歸并排序?qū)⒄麄€(gè)數(shù)組分成小數(shù)組,然后將排好序的小數(shù)組歸并以將整個(gè)數(shù)組排序;而快速排序是在將大數(shù)組分成小數(shù)組的時(shí)候排序,當(dāng)小數(shù)組小到不可再分的時(shí)候,排序也就完成了。
1.首先選擇一個(gè)中間元素(一般選左端或者右端)。
2.分別獲取除中間元素外的左右兩端的索引。
3.由左右兩端逐漸向中間迭代,每迭代一步比較一下索引中的元素和中間元素,當(dāng)左邊出現(xiàn)比中間元素大的元素的時(shí)候,暫停左邊的迭代,當(dāng)右邊迭代出比中間元素小的元素的時(shí)候,右邊迭代也暫停,交換左右兩邊的元素。
4.重復(fù)步驟3,直到左右兩邊的索引相遇,然后將中間元素移動(dòng)到中間,這時(shí)中間元素左邊的元素都比它小,右邊的元素都比它大。
5.將上面的中間元素左右兩邊當(dāng)成兩個(gè)數(shù)組,分別進(jìn)行上述過(guò)程。
6.重復(fù)以上步驟直到數(shù)組不可再分。

完整代碼
public class Quick {
    
    public static void sort(Comparable[] a){
        sort(a,0,a.length-1);
    }
    
    private static void sort(Comparable[] a,int low, int high){
        if (high <= low) return;
        int j = partition(a,low,high);
        sort(a,low,j-1);
        sort(a,j+1,high);
    }
    
    private static int partition(Comparable[] a, int low, int high){
        //將數(shù)組切分為a[lo..i-1], a[i], a[i+1..hi]
        int i= lo, j = hi+1; //左右掃描指針
       Comparable v = a[lo];//切分元素

       while (true)
      {//掃描左右,檢查掃描是否結(jié)束并交換元素
           while (less(a[++i], v))  if (i == hi) break;
           while (less(v, a[--j]))  if (j == lo) break;
           if (i >= j) break;
           exch(a, i, j);
       }
        exch(a,lo,j); //將v = a[j]放入正確位置
        return j;  //a[lo..j-1] <= a[j] <= a[j+1..hi]達(dá)成
    }
    
    public static void main(String[] args){
        Integer[] a = {9,8,7,6,5,4,3,2,1};
        sort(a, 0, a.length-1);
        for (Integer i: a){
            System.out.println(i);
        }
    }
    
    private static boolean less(Comparable v, Comparable w){
        return v.compareTo(w) < 0;
    }
    
    
    private static void exchange(Comparable[] a, int i, int j){
        Comparable t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
    
    
    private static void show(Comparable[] a){
        for (int i = 0; i < a.length; i++)
            System.out.print(a[i]);
        System.out.println();
    }
    
    
    public static boolean isSorted(Comparable[] a){
        for (int i = 1; i < a.length; i++){
            if (less(a[i],a[i-1])) return false;
        }
        return true;
    }
}

比較快速排序和歸并排序的速度
命令:

%   java Main Merge Quick 10000 1000
運(yùn)行結(jié)果
快速排序算法的改進(jìn)

快速排序自被C.A.R Hoare在1960年發(fā)明后,就不斷的有人試圖改進(jìn)它,但是由于快速排序已經(jīng)“so well-balanced”,改進(jìn)所帶來(lái)的優(yōu)化往往都被副作用抵消了,比如《算法》一書(shū)中對(duì)快速排序的實(shí)現(xiàn)中在排序之前會(huì)先隨機(jī)打亂數(shù)組來(lái)避免一種極端情況——當(dāng)我們每一次選擇的中間元素都恰好是最小元素時(shí),該算法會(huì)變的像選擇排序,從而導(dǎo)致時(shí)間復(fù)雜度變成N的平方。但是筆者在測(cè)試的時(shí)候發(fā)現(xiàn)運(yùn)行上面的指令時(shí)隨機(jī)打亂數(shù)組所花掉的時(shí)間幾乎使得運(yùn)行時(shí)間加倍,而事實(shí)上出現(xiàn)這種極端情況的概率比你的電腦在排序時(shí)突然被閃電擊中的概率都要小的多(這個(gè)flag不是我立的,我以后不隨隨便便排序了)。

但是依然有人找到了一些有用的改進(jìn)方式:

1.第一種改進(jìn)方案是說(shuō)由于插入排序在小數(shù)組的時(shí)候會(huì)比快速排序快,所以在分成小數(shù)組的時(shí)候使用插入排序,然而筆者在自己的電腦上測(cè)試的時(shí)候發(fā)現(xiàn)無(wú)論是大數(shù)組還是小數(shù)組,快速排序都比插入排序要快得多,按照這種方式修改的快速排序也變慢了,所以存疑。

2.實(shí)際應(yīng)用中我們排序的數(shù)組常常含有大量的重復(fù)元素,例如將上千萬(wàn)人員的資料按照生日排序,那就必然會(huì)有大量的重復(fù)的數(shù)值(畢竟一百年里面也就四萬(wàn)多天,分配給上千萬(wàn)人作生日,自然有大量重復(fù)),于是有人提出與其將數(shù)組二分,不如分成三部分,一部分小于中間值,一部分大于中間值,一部分等于中間值,此算法被稱為三向切分的快速排序,以下是代碼:

public class Quick3Way {

    public static void sort(Comparable[] a){
        sort(a,0,a.length-1);
    }
    
    private static void sort(Comparable[] a,int low, int high){
        if (high <= low) return;
        int lt = low, i = low+1,gt = high;
        Comparable v = a[low];
        while (i <= gt){
            int cmp = a[i].compareTo(v);
            if      (cmp < 0)exchange(a,lt++,i++);
            else if (cmp > 0)exchange(a,i,gt--);
            else    i++;
        }
                     sort(a,low,lt-1);
        if (gt<high) sort(a,gt+1,high);
    }
    
    public static void main(String[] args){
        Integer[] a = {9,8,7,6,5,4,3,2,1};
        sort(a, 0, a.length-1);
        for (Integer i: a){
            System.out.println(i);
        }
    }
    
    private static boolean less(Comparable v, Comparable w){
        return v.compareTo(w) < 0;
    }
      
    private static void exchange(Comparable[] a, int i, int j){
        Comparable t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
     
    private static void show(Comparable[] a){
        for (int i = 0; i < a.length; i++)
            System.out.print(a[i]);
        System.out.println();
    }
    
    public static boolean isSorted(Comparable[] a){
        for (int i = 1; i < a.length; i++){
            if (less(a[i],a[i-1])) return false;
        }
        return true;
    }    
}

當(dāng)使用隨機(jī)生成的由0~100組成的大小為一萬(wàn)的數(shù)組排序一千次時(shí)改進(jìn)方法與原方法所花費(fèi)的時(shí)間如下:

運(yùn)行結(jié)果

對(duì)于標(biāo)準(zhǔn)的快速排序,隨著數(shù)組規(guī)模的增大其運(yùn)行時(shí)間會(huì)趨于平均運(yùn)行時(shí)間,大幅度偏離的情況非常罕見(jiàn)。而三向切分的快速排序?qū)τ诖罅恐貜?fù)元素的數(shù)組來(lái)說(shuō)運(yùn)行時(shí)間由線性對(duì)數(shù)級(jí)別降低到了線性級(jí)別,并且和元素的排列沒(méi)有關(guān)系。由于在平常使用中對(duì)含有大量重復(fù)元素的數(shù)組排序的情況很常見(jiàn),所以擁有對(duì)重復(fù)元素的適應(yīng)性的三向分切的快速排序成為了排序庫(kù)函數(shù)的最佳選擇。
經(jīng)過(guò)精心優(yōu)化的快速排序在絕大多數(shù)計(jì)算機(jī)的絕大多數(shù)應(yīng)用中都比其他算法要快,它在當(dāng)前業(yè)界的廣泛使用正說(shuō)明了這一點(diǎn)。

總結(jié)一下當(dāng)前學(xué)習(xí)過(guò)的排序算法的速度:

在給千萬(wàn)級(jí)別的數(shù)組排序的情況下:
Quick > Merge > Shell > Insertion > Selection

資源以及參考

普林斯頓大學(xué)算法課程以其教材《算法》第四版

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

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

  • 一. 寫(xiě)在前面 要學(xué)習(xí)算法,“排序”是一個(gè)回避不了的重要話題,在分析完并查集算法和常用數(shù)據(jù)結(jié)構(gòu)之后,今天我們終于可...
    Leesper閱讀 2,545評(píng)論 0 40
  • 1.插入排序—直接插入排序(Straight Insertion Sort) 基本思想: 將一個(gè)記錄插入到已排序好...
    依依玖玥閱讀 1,270評(píng)論 0 2
  • 概述 排序有內(nèi)部排序和外部排序,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進(jìn)行排序,而外部排序是因排序的數(shù)據(jù)很大,一次不能容納全部...
    蟻前閱讀 5,213評(píng)論 0 52
  • Ba la la la ~ 讀者朋友們,你們好啊,又到了冷鋒時(shí)間,話不多說(shuō),發(fā)車(chē)! 1.冒泡排序(Bub...
    王飽飽閱讀 1,809評(píng)論 0 7
  • 文|小Cindy 每次在朋友圈看到有人打卡已在某某app背了xxx個(gè)單詞,或者是已在某某app閱讀xxx天都覺(jué)得這...
    故園東望路漫漫Cy閱讀 373評(píng)論 0 1