十大排序算法總結(jié)

@[toc]

0. 前言

排序算法中涉及到了兩個(gè)概念:

原地排序:根據(jù)算法對(duì)內(nèi)存的消耗情況髓涯,可以將算法分為原地排序和非原地排序漫玄,原地排序特指空間復(fù)雜度為 O(1) 的排序。

排序算法的穩(wěn)定性:例如排序一個(gè)數(shù)組 [1, 5, 3, 7, 4, 9, 5],數(shù)組中有兩個(gè) 5彻磁,排序之后是 [1, 3, 4, 5, 5, 7, 9],如果排序之后的兩個(gè) 5 的前后順序沒有發(fā)生變化狸捅,那么稱這個(gè)排序是穩(wěn)定的衷蜓,反之則是不穩(wěn)定的。

1. 冒泡排序

冒泡排序是很經(jīng)典的排序算法了尘喝,相鄰的兩個(gè)數(shù)據(jù)依次進(jìn)行比較并交換位置磁浇。遍歷一遍數(shù)組后,則有一個(gè)數(shù)據(jù)排序完成朽褪,然后再遍歷 n 次置吓,排序完成无虚。示意圖如下:

在這里插入圖片描述

代碼實(shí)現(xiàn):

public class BubbleSort {

    private static void bubbleSort(int[] data){
        int length = data.length;
        for (int i = length - 1; i > 0; i --) {
            //判斷是否有數(shù)據(jù)交換,如果沒有則提前退出
            boolean flag = false;
            for (int j = 0; j < i; j ++) {
                if (data[j] > data[j + 1]){
                    int temp = data[j];
                    data[j] = data[j + 1];
                    data[j + 1] = temp;
                    flag = true;
                }
            }
            if (!flag){
                break;
            }
        }
    }
}

2. 選擇排序

將要排序的數(shù)據(jù)分為了已排序區(qū)間和未排序區(qū)間衍锚,每次從未排序區(qū)間找到最小值友题,然后將其放到已排序區(qū)間的末尾,循環(huán)遍歷未排序區(qū)間則排序完成戴质。

示意圖如下:

在這里插入圖片描述

代碼實(shí)現(xiàn):

public class SelectionSort {

    public static void selectionSort(int[] data){
        int length = data.length;
        for (int i = 0; i < length - 1; i++) {
            int min = i;
            for (int j = i + 1; j < length; j++) {
                if (data[min] > data[j]){
                    min = j;
                }
            }
            int temp = data[min];
            data[min] = data[i];
            data[i] = temp;
        }
    }
}

3. 插入排序

和選擇排序類似度宦,插入排序也將數(shù)據(jù)分為了已排序區(qū)間和未排序區(qū)間,遍歷未排序區(qū)間告匠,每次取一個(gè)數(shù)據(jù)斗埂,將其插入到已排序區(qū)間的合適位置,讓已排序區(qū)間一直保持有序凫海,直到未排序區(qū)間遍歷完排序則完成呛凶。

示意圖如下:

在這里插入圖片描述

代碼實(shí)現(xiàn):

public class InsertionSort {

    public static void insertionSort(int[] data){
        int length = data.length;
        for (int i = 0; i < length - 1; i++) {
            int val = data[i + 1];
            int j = i + 1;
            while (j > 0 && data[j - 1] > val){
                data[j] = data[j - 1];
                j --;
            }
            data[j] = val;
        }
    }
}

插入排序?yàn)槭裁幢让芭菖判蚋S茫?/p>

這兩種排序的時(shí)間復(fù)雜度都是一樣的,最好情況是 O(n)行贪,最壞情況是 O(n2)漾稀,但是在實(shí)際的生產(chǎn)中,插入排序使用更多建瘫,原因在于兩者數(shù)據(jù)交換的次數(shù)不同崭捍。冒泡排序需要進(jìn)行三次交換,插入排序只要一次:

//冒泡排序數(shù)據(jù)交換
if (data[j] > data[j + 1]){
    int temp = data[j];
    data[j] = data[j + 1];
    data[j + 1] = temp;
    flag = true;
}

//插入排序數(shù)據(jù)交換
while (j > 0 && data[j - 1] > val){
    data[j] = data[j - 1];
    j --;
}

在數(shù)據(jù)量較大的時(shí)候啰脚,兩者性能差距就體現(xiàn)出來了殷蛇。

4. 希爾排序

希爾排序其實(shí)是插入排序的一種優(yōu)化,其思路是將排序的數(shù)組按照一定的增量將數(shù)據(jù)分組橄浓,每個(gè)分組用插入排序進(jìn)行排序粒梦,然后增量逐步減小,當(dāng)增量減小為1的時(shí)候荸实,算法便終止匀们,所以希爾排序又叫做“縮小增量排序”。

示意圖如下:

在這里插入圖片描述

圖中的示例准给,每次依次將數(shù)組分為若干組泄朴,每組分別進(jìn)行插入排序。代碼實(shí)現(xiàn)如下:

public class ShellSort {

    public static void shellSort(int[] data) {

        int length = data.length;
        int step = length / 2;
        while (step >= 1){
            for (int i = step; i < length; i++) {
                int val = data[i];
                int j = i - step;
                for (; j >= 0; j -= step){
                    if (data[j] > val){
                        data[j + step] = data[j];
                    }
                    else {
                        break;
                    }
                }
                data[j + step] = val;
            }
            step = step / 2;
        }
    }
}

5. 歸并排序

歸并排序使用到了分治思想露氮,分治思想即將大的問題分解成小的問題祖灰,小的問題解決了,大的問題也就解決了畔规。蘊(yùn)含分治思想的問題局扶,一般可以使用遞歸技巧來實(shí)現(xiàn)。

歸并排序的思路是:首先將數(shù)組分解,局部進(jìn)行排序详民,然后將排序的結(jié)果進(jìn)行合并,這樣整個(gè)數(shù)組就有序了陌兑,你可以結(jié)合下圖理解:

在這里插入圖片描述

代碼實(shí)現(xiàn):

public class MergeSort {

    public static void mergeSort(int[] data){
        mergeInternally(data, 0, data.length - 1);
    }

    private static void mergeInternally(int[] data, int p, int r){
        if (p >= r){
            return;
        }
        int q = (p + r) / 2;
        //分治遞歸
        mergeInternally(data, p, q);
        mergeInternally(data, q + 1, r);
        //結(jié)果合并
        merge(data, p, q, r);
    }

    private static void merge(int[] data, int p, int q, int r){
        int[] temp = new int[r - p + 1];
        int k = 0;
        int i = p;
        int j = q + 1;
        //比較并合并
        while (i <= q && j <= r){
            if (data[i] < data[j]){
                temp[k ++] = data[i ++];
            }
            else {
                temp[k ++] = data[j ++];
            }
        }
        //合并可能出現(xiàn)的剩余元素
        int start = i;
        int end = q;
        if (j <= r){
            start = j;
            end = r;
        }
        while (start <= end){
            temp[k ++] = data[start ++];
        }
        //拷貝回原數(shù)組
        if (r - p + 1 >= 0) {
            System.arraycopy(temp, 0, data, p, r - p + 1);
        }
    }
}

6. 快速排序

快速排序也用到了分治的思想沈跨,只不過它和歸并排序的思路剛好是相反的,快速排序使用數(shù)組中一個(gè)數(shù)據(jù)作為分區(qū)點(diǎn)(一般可以選取數(shù)組第一個(gè)或最后一個(gè)元素)兔综,比分區(qū)點(diǎn)小的饿凛,放在左側(cè),比分區(qū)點(diǎn)大的软驰,放在右側(cè)涧窒。然后左右兩側(cè)的數(shù)據(jù)再次選擇分區(qū)點(diǎn),循環(huán)進(jìn)行這個(gè)操作锭亏,直到排序完成纠吴。

示意圖如下(圖中是以第一個(gè)元素作為分區(qū)點(diǎn)):

在這里插入圖片描述

代碼實(shí)現(xiàn):

public class QuickSort {
    public static void quickSort(int[] data){
        quickSortInternally(data, 0, data.length - 1);
    }

    private static void quickSortInternally(int[] data, int p, int r){
        if (p >= r){
            return;
        }
        int q = partition(data, p, r);
        quickSortInternally(data, p, q - 1);
        quickSortInternally(data, q + 1, r);
    }

    /**
     * 獲取分區(qū)點(diǎn)函數(shù)
     */
    private static int partition(int [] data, int p, int q){
        int pivot = data[q];
        int i = 0;
        int j = 0;
        while (j < q){
            if (data[j] <= pivot){
                swap(data, i, j);
                i ++;
            }
            j ++;
        }
        swap(data, i, q);
        return i;
    }
    /**
     * 交換數(shù)組兩個(gè)元素
     */
    private static void swap(int[] data, int i, int j){
        int temp = data[i];
        data[i] = data[j];
        data[j] = temp;
    }
}

7. 堆排序

基于堆的排序比較常用,時(shí)間復(fù)雜度為 O(nlogn)慧瘤,并且是原地排序戴已,主要的步驟分為建堆和排序。

建堆

思路是從堆中第一個(gè)非葉子節(jié)點(diǎn)锅减,依次從上往下進(jìn)行堆化糖儡,如下圖:

在這里插入圖片描述

排序

建堆完成之后,假設(shè)堆中元素個(gè)數(shù)為 n怔匣,堆頂元素即是最大的元素握联,這時(shí)候直接將堆頂元素和堆中最后一個(gè)元素進(jìn)行交換,然后將剩余的 n - 1 個(gè)元素構(gòu)建成新的堆每瞒,依次類推金闽,直到堆中元素減少至 1,則排序完成剿骨。示意圖如下:

在這里插入圖片描述

代碼實(shí)現(xiàn):

public class HeapSort {

    /**
     * 排序
     */
    public void heapSort(int[] data){
        int length = data.length;
        if (length <= 1){
            return;
        }
        buildHeap(data);
        while (length > 0){
            swap(data, 0, -- length);
            heapify(data, length, 0);
        }
    }

    /**
     * 建堆
     */
    private void buildHeap(int[] data){
        int length = data.length;
        for (int i = (length - 2) / 2; i >= 0; i --) {
            heapify(data, length, i);
        }
    }

    /**
     * 堆化函數(shù)
     */
    private void heapify(int[] data, int size, int i){
        while (true){
            int max = i;
            if ((2 * i + 1) < size && data[i] < data[2 * i + 1]) {
                max = 2 * i + 1;
            }
            if ((2 * i + 2) < size && data[max] < data[2 * i + 2]) {
                max = 2 * i + 2;
            }
            if (max == i){
                break;
            }
            swap(data, i, max);
            i = max;
        }
    }

    /**
     * 交換數(shù)組中兩個(gè)元素
     */
    private void swap(int[] data, int i, int j){
        int temp = data[i];
        data[i] = data[j];
        data[j] = temp;
    }
}

8. 桶排序

桶排序并不是基于數(shù)據(jù)比較的呐矾,因此比較的高效,時(shí)間復(fù)雜度接近 O(n)懦砂,但是相應(yīng)地蜒犯,應(yīng)用的條件十分苛刻。其思路非常的簡(jiǎn)單:將要排序的數(shù)據(jù)分到各個(gè)有序的桶內(nèi)荞膘,數(shù)據(jù)在桶內(nèi)進(jìn)行排序罚随,然后按序取出,整個(gè)數(shù)據(jù)就是有序的了羽资。

最好情況下淘菩,數(shù)據(jù)被均勻的分到各個(gè)桶中,最壞情況是數(shù)據(jù)全都被分到一個(gè)桶中。

下面是一個(gè)桶排序的示例:

public class BucketSort {

    /**
     * 測(cè)試場(chǎng)景:數(shù)組中有10000個(gè)數(shù)據(jù)潮改,范圍在(0-100000)
     * 使用100個(gè)桶狭郑,每個(gè)桶存放的數(shù)據(jù)范圍為:0-999, 1000-1999, 2000-2999,依次類推
     */
    public static void bucketSort(int[] data){
        //新建100個(gè)桶
        int bucketSize = 100;
        ArrayList<ArrayList<Integer>> buckets = new ArrayList<>(bucketSize);
        for (int i = 0; i < bucketSize; i++) {
            buckets.add(new ArrayList<>());
        }
        //遍歷數(shù)據(jù)汇在,將數(shù)據(jù)放到桶中
        for (int i : data) {
            buckets.get(i / 1000).add(i);
        }
        //在桶內(nèi)部進(jìn)行排序
        int k = 0;
        for (int i = 0; i < bucketSize; i++) {
            ArrayList<Integer> list = buckets.get(i);
            Integer[] num = list.toArray(new Integer[1]);
            Arrays.sort(num);
            //拷貝到data中
            for (int n : num) {
                data[k++] = n;
            }
        }
    }
    //測(cè)試
    public static void main(String[] args) {
        Random random = new Random();
        int[] data = new int[10000];
        for (int i = 0; i < data.length; i++) {
            data[i] = random.nextInt(100000);
        }
        BucketSort.bucketSort(data);
        System.out.println(Arrays.toString(data));
    }
}

9. 計(jì)數(shù)排序

計(jì)數(shù)排序其實(shí)是一種特殊的桶排序翰萨,適用于數(shù)據(jù)的區(qū)間不是很大的情況。

例如給 10 萬人按照年齡進(jìn)行排序糕殉,我們知道年齡的區(qū)間并不是很大亩鬼,最小的 0 歲,最大的可以假設(shè)為 120 歲阿蝶,那么我們可以新建 121 個(gè)桶雳锋,掃描一遍數(shù)據(jù),將年齡相同的放到一個(gè)桶中羡洁,然后按序從桶中將數(shù)據(jù)取出玷过,這樣數(shù)據(jù)就有序了。

計(jì)數(shù)排序的基本思路如下:

在這里插入圖片描述

代碼實(shí)現(xiàn):

public class CountingSort {

    private static void countingSort(int[] data){
        int length = data.length;
        //找到數(shù)組的最大值
        int max = data[0];
        for (int i : data){
            if (max < i){
                max = i;
            }
        }
        //新建一個(gè)計(jì)數(shù)數(shù)組筑煮,大小為max+1
        //count數(shù)組的下標(biāo)對(duì)應(yīng)data的值冶匹,存儲(chǔ)的值為對(duì)應(yīng)data值的個(gè)數(shù)
        int[] count = new int[max + 1];
        for (int i : data){
            count[i] ++;
        }
        //根據(jù)count數(shù)組取出數(shù)據(jù)
        int k = 0;
        for (int i = 0; i < count.length; i++) {
            while (count[i] != 0){
                data[k ++] = i;
                count[i] --;
            }
        }
    }
}

10. 基數(shù)排序

基數(shù)排序適用于位數(shù)較多的數(shù)字或者字符串,思路是將排序的數(shù)據(jù)按位拆分咆瘟,每一位單獨(dú)按照穩(wěn)定的排序算法進(jìn)行比較嚼隘,如下圖:

在這里插入圖片描述

圖中的示例,以每個(gè)數(shù)字為下標(biāo)袒餐,建了 10 個(gè) “桶”飞蛹,每個(gè)桶是一個(gè)隊(duì)列(也可以是數(shù)組),然后將要排序的數(shù)據(jù)按位加入到隊(duì)列中灸眼,然后出隊(duì)卧檐,比較完每一位,則排序完成焰宣。

代碼實(shí)現(xiàn):

public class RadixSort {

    private static void radixSort(int[] data) {
        int maxDigit = maxDigit(data);

        //新建并初始化10個(gè)桶
        Queue<Integer>[] queues = new LinkedList[10];
        for (int i = 0; i < queues.length; i++) {
            queues[i] = new LinkedList<>();
        }

        for (int i = 0, mod = 1; i < maxDigit; i ++, mod *= 10) {
            for (int n : data){
                int m = (n / mod) % 10;
                queues[m].add(n);
            }

            int count = 0;
            for (Queue<Integer> queue : queues) {
                while (queue.size() > 0) {
                    data[count++] = queue.poll();
                }
            }
        }
    }

    /**
     * 獲取數(shù)組最大位數(shù)
     */
    private static int maxDigit(int[] data){
        int maxDigit = data[0];
        for (int i : data){
            if (maxDigit < i){
                maxDigit = i;
            }
        }
        return String.valueOf(maxDigit).length();
    }
}

在我的 Github 上面有更加詳細(xì)的數(shù)據(jù)結(jié)構(gòu)與算法代碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末霉囚,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子匕积,更是在濱河造成了極大的恐慌盈罐,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,589評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件闪唆,死亡現(xiàn)場(chǎng)離奇詭異盅粪,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)悄蕾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門票顾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事奠骄《雇” “怎么了?”我有些...
    開封第一講書人閱讀 165,933評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵含鳞,是天一觀的道長(zhǎng)影锈。 經(jīng)常有香客問我,道長(zhǎng)民晒,這世上最難降的妖魔是什么精居? 我笑而不...
    開封第一講書人閱讀 58,976評(píng)論 1 295
  • 正文 為了忘掉前任锄禽,我火速辦了婚禮潜必,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘沃但。我一直安慰自己磁滚,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評(píng)論 6 393
  • 文/花漫 我一把揭開白布宵晚。 她就那樣靜靜地躺著垂攘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪淤刃。 梳的紋絲不亂的頭發(fā)上晒他,一...
    開封第一講書人閱讀 51,775評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音逸贾,去河邊找鬼陨仅。 笑死,一個(gè)胖子當(dāng)著我的面吹牛铝侵,可吹牛的內(nèi)容都是我干的灼伤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,474評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼咪鲜,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼狐赡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起疟丙,我...
    開封第一講書人閱讀 39,359評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤颖侄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后享郊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體发皿,經(jīng)...
    沈念sama閱讀 45,854評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評(píng)論 3 338
  • 正文 我和宋清朗相戀三年拂蝎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了穴墅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,146評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖玄货,靈堂內(nèi)的尸體忽然破棺而出皇钞,到底是詐尸還是另有隱情,我是刑警寧澤松捉,帶...
    沈念sama閱讀 35,826評(píng)論 5 346
  • 正文 年R本政府宣布夹界,位于F島的核電站,受9級(jí)特大地震影響隘世,放射性物質(zhì)發(fā)生泄漏可柿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評(píng)論 3 331
  • 文/蒙蒙 一丙者、第九天 我趴在偏房一處隱蔽的房頂上張望复斥。 院中可真熱鬧,春花似錦械媒、人聲如沸目锭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽痢虹。三九已至,卻和暖如春主儡,著一層夾襖步出監(jiān)牢的瞬間奖唯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工糜值, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留丰捷,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,420評(píng)論 3 373
  • 正文 我出身青樓臀玄,卻偏偏與公主長(zhǎng)得像瓢阴,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子健无,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評(píng)論 2 356

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