排序算法介紹

在介紹排序之前肮街,首先來介紹幾個在每個排序算法中都需要使用的方法,我將它們單獨寫在了一個類中,它們分別是 less(), exchange(), isSort() 方法敦间,less() 方法用于判斷兩個元素的大小炸渡, exchange() 用于交換兩個元素的位置娜亿,isSort() 方法用于判斷當前數(shù)組是否有序,它們的實現(xiàn)如下所示

package com.sort;

@SuppressWarnings("rawtypes")
public class Sort {

    @SuppressWarnings("unchecked")
    public static boolean less(Comparable a, Comparable b) {
        return a.compareTo(b) < 0;
    }

    public static void exchange(Comparable[] a, int i, int j) {
        if (i == j)
            return;
        Comparable t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

    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ù)據(jù)類型設(shè)置為 Comparable 是為了兼容更多種類型的數(shù)據(jù)蚌堵,比如這樣寫买决,我們可以對任何實現(xiàn)了 Comparable 接口的數(shù)據(jù)進行操作,比如系統(tǒng)系統(tǒng)的 Integer吼畏,Double督赤,String 等。如果我們想要對自定義的數(shù)據(jù)進行操作泻蚊,那么就實現(xiàn) Comparable 接口并且重寫 compareTo() 方法躲舌,定義自己的判斷規(guī)則即可。

選擇排序

這種排序是最簡單的排序算法之一性雄,另一個是下面將要介紹的插入排序没卸。選擇排序的基本思想是:首先找到數(shù)組中最小的那個元素,其次秒旋,將它和數(shù)組的第一個元素交換位置约计。再次,在剩下的元素中找到最小的元素滩褥,將它和第二個元素交換位置病蛉。如此往復(fù),直到整個數(shù)組排序。

對于一個長度為 N 的數(shù)組铺然,選擇排序需要大約 N^2/2 次比較 和 N 次交換俗孝,所以說它的執(zhí)行時間總是和 N^2 成正比的。

這種算法有兩個特點

  1. 運行時間和輸入無關(guān)魄健。也就是說我們?yōu)榱苏页鲎钚〉哪莻€元素而遍歷一遍數(shù)組并沒有為下次掃描提供任何有用的信息赋铝。這也就意味著,一個已經(jīng)有序的數(shù)組或者主鍵全部相等的數(shù)組和一個元素隨機排列的數(shù)組所用的排序時間竟然一樣長沽瘦。
  2. 數(shù)據(jù)的移動是最少的革骨。因為我們每次交換都會改變兩個元素的值,所以選擇排序只會進行 N 次交換析恋,交換次數(shù)和數(shù)組的大小呈現(xiàn)線性關(guān)系良哲,這個特征是選擇排序獨有的,其他排序算法都不具備此特性助隧。

下面來看看選擇排序的實現(xiàn)

public final class SelectionSort {
    @SuppressWarnings("rawtypes")
    public static void sort(Comparable[] a) {
        if (Sort.isSorted(a)) {
            return;
        }
        int length = a.length;
        for (int i = 0; i < length; i++) {
            int min = i;
            for (int j = i + 1; j < length; j++) {
                if (Sort.less(a[j], a[min])) {
                    min = j;
                }
            }
            if (min != i) {
                Sort.exchange(a, i, min);
            }

        }
    }
}

插入排序

插入排序和選擇排序的共同點是筑凫,當前索引的左邊的所有元素都是有序的,但是不同的是插入排序只對當前索引左邊的值進行操作并村,而選擇排序是對索引右邊的數(shù)進行操作巍实。

在性能上,插入排序所需要的時間取決于輸入元素的初始順序哩牍,如果一個很大而且其中的元素已經(jīng)基本有序的數(shù)組棚潦,插入排序所需的時間會非常短。在以下幾種情況下膝昆,插入排序是一個非常高效的算法

  1. 數(shù)組中每個元素距離它的最終位置都不遠
  2. 一個有序的大數(shù)組接一個小數(shù)組
  3. 數(shù)組中只有那么幾個元素位置不正確

關(guān)于選擇排序和插入排序丸边,如果是針對一個隨機的數(shù)組,那么這兩種排序算法所需要的時間之比是一個很小的常數(shù)外潜,如果是針對部分有序的數(shù)組原环,差距將會更加明顯。

插入排序的實現(xiàn)

package com.sort;

public class InsertSort {

    @SuppressWarnings("rawtypes")
    public static void sort(Comparable[] a) {

        int length = a.length;

        for (int i = 1; i < length; i++) {
            for (int j = i; j > 0 && Sort.less(a[j], a[j - 1]); j--) {
                Sort.exchange(a, j, j - 1);
            }
        }
    }

}

希爾排序

希爾排序是基于插入排序的处窥,它是一種快速的插入排序嘱吗。想象一下,對于一個很大的數(shù)組滔驾,最小的元素在數(shù)組的末尾谒麦,如果使用插入排序,它將一步步的移動到數(shù)組的開頭哆致,這是十分消耗時間的绕德。所以希爾排序主張將數(shù)組中任意間隔為 h 的元素排成有序,稱之為 h 有序數(shù)組摊阀,換言之就是將所有間隔為 h 的元素組成一個小的數(shù)組耻蛇,如果 h 很大踪蹬,那么我們一次就可以把較小的元素移動到很遠的地方,而不是一步步的移動臣咖。

希爾排序高效的原因是它權(quán)衡了子數(shù)組的規(guī)模和有序性跃捣。當然希爾排序的性能還與 h 有關(guān),對于不同的 h 表現(xiàn)出不同的速度夺蛇,但是無法確定哪一種是最好的赵抢,所以這里選取 h 從 N/3 遞減至 1.

希爾排序的運行時間是達不到 平方級別的铸磅,它在最壞的情況下 和 N^3/2 成正比缚够。

希爾排序的實現(xiàn)

package com.sort;

public class ShellSort {

    @SuppressWarnings("rawtypes")
    public static void sort(Comparable[] a) {

        int length = a.length;
        int h = 1;

        while (h < length / 3)
            h = 3 * h + 1;

        while (h >= 1) {
            for (int i = h; i < length; i++) {
                for (int j = i; j >= h && Sort.less(a[j], a[j - h]); j -= h) {

                    Sort.exchange(a, j, j - h);
                }
            }
            h = h / 3;
        }
    }
}

歸并排序

實際上煮剧,當問題的規(guī)模到達一定的程度,上面的算法就已經(jīng)無能為力了甚脉,尤其是 插入排序和選擇排序丸升,所以下面來介紹歸并排序。

歸并排序的主要思想是將一個數(shù)組分割成兩部分牺氨,分別進行排序发钝,然后將結(jié)果歸并起來.

歸并排序的優(yōu)點是能夠保證將任意長度為 N 的數(shù)組排序時間和 NlogN 成正比,缺點是他所需的 額外空間和 N 成正比波闹,也就是說排序需要借助于輔助空間。

下面來看看代碼

package com.sort;

public class MergeSort {
    private static Comparable[] aux;

    public static void sort(Comparable[] a) {
        aux = new Comparable[a.length];
        sort(a, 0, a.length - 1);
    }

    private static void sort(Comparable[] a, int low, int high) {
        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 void merge(Comparable[] a, int low, int mid, int high) {
        int i = low;
        int j = mid + 1;

        for (int k = low; k <= high; k++) {
            aux[k] = a[k];
        }

        for (int k = low; k <= high; k++) {
            if (i > mid)
                a[k] = aux[j++];
            else if (j > high)
                a[k] = aux[i++];
            else if (Sort.less(aux[i], aux[j]))
                a[k] = aux[i++];
            else
                a[k] = aux[j++];
        }
    }

}

分析一下以上的代碼涛碑,首先申請一個和原數(shù)組 a 相同大小的輔助空間精堕,調(diào)用 sort() 方法,遞歸分割數(shù)組蒲障,直到 high <= low歹篓,這說明數(shù)組中只存在一個元素了,遞歸分割的意義在于不斷縮小數(shù)組的規(guī)模揉阎,然后對數(shù)組進行排序合并庄撮,用二叉樹來描述這一過程最為合適,左右孩子排序后合成根節(jié)點毙籽。

快速排序

快速排序是當前最流行的排序算法洞斯,是使用最廣泛的排序算法了。之所以它這么受歡迎是因為它只需要一個很小的輔助棧坑赡,而且將一個長度為N 的數(shù)組排序所需時間和 NlgN 成正比烙如。之前學(xué)過的所有算法都無法將這兩個優(yōu)點結(jié)合起來,但是快速排序做到了毅否。

快速排序?qū)崿F(xiàn)的思想是將一個數(shù)組分成兩個數(shù)組亚铁,將兩部分進行獨立排序。但是這個分割不和歸并排序一樣螟加,這里的切分位置是取決于數(shù)組的內(nèi)容的徘溢。切分的過程需要滿足以下三個條件

  1. 對于某個 j, a[j] 已經(jīng)排定吞琐;
  2. a[low] 到 a[j-1] 的所有元素都不大于 a[j];
  3. a[j+1] 到 a[high] 的所有元素都不小于 [j]然爆;

然后遞歸的調(diào)用切分就可以實現(xiàn)排序站粟,因為每次切分都能排定一個元素。要完成這個實現(xiàn)施蜜,需要實現(xiàn)切分方法卒蘸,一般策略是先隨意的取 a[low] 作為切分元素。

算法 如下

public class QuickSort {

    public static void sort(Comparable[] a) {
        if (Sort.isSorted(a)) {
            return;
        }

        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) {
        int i = low, j = high + 1;
        Comparable v = a[low];
        while (true) {
            while (Sort.less(a[++i], v))
                if (i == high)
                    break;
            while (Sort.less(v, a[--j]))
                if (j == low)
                    break;
            if (i >= j)
                break;

            Sort.exchange(a, i, j);
        }

        Sort.exchange(a, low, j);
        return j;
    }

}

算法中的 partition() 函數(shù)主要實現(xiàn)的就是將 a[low] 這個元素排定到指定的位置并且將該位置作為返回值翻默,返回給 sort() 函數(shù)缸沃,用于分割數(shù)組,隨著 sort() 函數(shù)的遞歸調(diào)用修械,會將每個元素排定到指定位置趾牧,從而實現(xiàn)排序。

快速排序雖然有很多優(yōu)點肯污,但是有一個潛在的缺點翘单,在切份不平衡時這個程序可能會極為低效。比如說第一次從最小的那個元素切分蹦渣,第二次從第二小的元素進行切分哄芜,這樣的話每次調(diào)用只會移除一個元素,這個時候性能就非常低了柬唯。所以在使用快速排序的時候最好首相將數(shù)組隨機打亂认臊,這樣的話會將上述概率降到最低。

實際上上述快速排序的實現(xiàn)還是有待改進的锄奢,比如說我們可以在數(shù)組特別小的時候失晴,使用插入排序,只需要在 sort() 方法中將遞歸的判定條件改成如下所示

if (high <= low + X){
    InsertSort.sort(a,low,high);
    return;
}   

X 可以是 5 - 15 內(nèi)的數(shù)拘央,這可以在一定程度上改善涂屁,尤其是在小數(shù)組基本有序的情況下。

當然灰伟,現(xiàn)在比較流行的還是三向切分排序拆又,這種算法主要應(yīng)對在數(shù)組中存在大量重復(fù)的情況,算法的基本思想就是首先選取到一個切分元素袱箱,一般是 a[low],然后取出該元素并命名為v遏乔。設(shè)置 lt 指向 low , gt 指向 high发笔,i 指向 low+1盟萨。 在 i <= gt 的時候執(zhí)行以下操作:

1.如果a[i] < v,那么將 a[i] 和 a[lt] 互換,并且lt 和 i 加 1 了讨。
2.如果a[i] > v捻激,那么將a[i] 與 a[gt] 互換制轰,并且 gt -1.
3.如果 a[i] = v ,那么直接 i +1,不執(zhí)行交換操作胞谭。

待程序跳出以上循環(huán)的時候垃杖,調(diào)用 sort() 方法,并且左邊的數(shù)組以 lt-1 位上界丈屹,右邊的數(shù)組以 gt+1 為下界调俘。這個時候就可以很明顯的看出來,每次切分就不會再將以前已經(jīng)排過序的元素納入數(shù)組旺垒,這樣在元素有大量重復(fù)的情況下可以顯著提高排序效率彩库,當然在隨機情況下,它的效率也是非常好的先蒋。下面來看看具體實現(xiàn)骇钦。

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)
                Sort.exchange(a, lt++, i++);
            else if (cmp > 0)
                Sort.exchange(a, i, gt--);
            else
                i++;
        }

        sort(a, low, lt - 1);
        sort(a, gt + 1, high);
    }

}

各個排序算法的比較

為了驗證算法的優(yōu)劣,我對以上算法分別進行了 1 0000數(shù)量級竞漾,10 0000數(shù)量級眯搭,100 0000數(shù)量級的數(shù)組進行了測試,測試結(jié)果如下:

1.1 0000 數(shù)量級

選擇排序 : 0.112s
插入排序 : 0.16s
希爾排序 : 0.016s
歸并排序 : 0.011s
快速排序 : 0.008s

2.10 0000 數(shù)量級

選擇排序 : 11.19s
插入排序 : 16.521s
希爾排序 : 0.084 s
歸并排序 : 0.114s
快速排序 : 0.066s

3.100 0000 數(shù)量級
選擇排序 : 等了幾分鐘未果
插入排序 : 等了幾分鐘未果
希爾排序 : 2.022s
歸并排序 : 0.742s
快速排序 : 0.521s

上述用于測試的數(shù)據(jù)由 Random 函數(shù)生成的业岁,并且與數(shù)量級相乘鳞仙,比如說 10000 數(shù)量級就是用 (int) (Math.random() * 10000) 生成,由于數(shù)是隨機的笔时,所以上述時間也不是一個固定的值繁扎,但相差不會太大。

有上述實驗結(jié)果反映出來一些問題

  1. 選擇排序和插入排序?qū)σ?guī)模大的數(shù)組是無能為力的
  2. 規(guī)模越大糊闽,快速排序和歸并排序的優(yōu)勢就越大,尤其是快速排序
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末爹梁,一起剝皮案震驚了整個濱河市右犹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌姚垃,老刑警劉巖念链,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異积糯,居然都是意外死亡掂墓,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進店門看成,熙熙樓的掌柜王于貴愁眉苦臉地迎上來君编,“玉大人,你說我怎么就攤上這事川慌〕院伲” “怎么了祠乃?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長兑燥。 經(jīng)常有香客問我亮瓷,道長,這世上最難降的妖魔是什么降瞳? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任嘱支,我火速辦了婚禮,結(jié)果婚禮上挣饥,老公的妹妹穿的比我還像新娘除师。我一直安慰自己,他們只是感情好亮靴,可當我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布馍盟。 她就那樣靜靜地躺著,像睡著了一般茧吊。 火紅的嫁衣襯著肌膚如雪贞岭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天搓侄,我揣著相機與錄音瞄桨,去河邊找鬼。 笑死讶踪,一個胖子當著我的面吹牛芯侥,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播乳讥,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼柱查,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了云石?” 一聲冷哼從身側(cè)響起唉工,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎汹忠,沒想到半個月后淋硝,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡宽菜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年谣膳,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铅乡。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡继谚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出阵幸,到底是詐尸還是另有隱情犬庇,我是刑警寧澤僧界,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站臭挽,受9級特大地震影響捂襟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜欢峰,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一葬荷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧纽帖,春花似錦宠漩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至室囊,卻和暖如春雕崩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背融撞。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工盼铁, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人尝偎。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓饶火,卻偏偏與公主長得像,于是被迫代替她去往敵國和親致扯。 傳聞我的和親對象是個殘疾皇子肤寝,可洞房花燭夜當晚...
    茶點故事閱讀 43,562評論 2 349

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

  • 概述 排序有內(nèi)部排序和外部排序,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進行排序抖僵,而外部排序是因排序的數(shù)據(jù)很大醒陆,一次不能容納全部...
    蟻前閱讀 5,170評論 0 52
  • 1.插入排序—直接插入排序(Straight Insertion Sort) 基本思想: 將一個記錄插入到已排序好...
    依依玖玥閱讀 1,245評論 0 2
  • 概述排序有內(nèi)部排序和外部排序,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進行排序裆针,而外部排序是因排序的數(shù)據(jù)很大,一次不能容納全部的...
    Luc_閱讀 2,259評論 0 35
  • 7月4日 多云 這幾天寺晌,看到家長朋友們寫的親子日記世吨,感觸很深!有什么樣的家長就有什么樣的孩子呻征,家長朋...
    官越媽媽閱讀 122評論 0 0
  • 涵寶現(xiàn)在六歲耘婚,是一個比較欠缺安全感的小孩,和她三歲多的妹妹完全相反陆赋。她睡覺的時候不敢一個人睡在床的一頭沐祷;每晚大人不...
    pan02閱讀 148評論 0 0