11.經典O(n²)比較型排序算法

關注公號「碼哥字節」修煉技術內功心法,完整代碼可跳轉 GitHub:https://github.com/UniqueDong/algorithms.gitjava

摘要:排序算法提多了,不少甚至連名字你都沒聽過,好比猴子排序、睡眠排序等。最經常使用的:冒泡排序、選擇排序、插入排序、歸併排序、快速排序、計數排序、基數排序、桶排序。根據時間複雜度,咱們分三類來學習,今天要講的就是 冒泡、插入、選擇 排序算法。git

排序算法 時間複雜度 是否基於比較
冒泡、插入、選擇 O(n²)
快排、歸併 O(nlogn)
桶、計數、基數 O(n)

十種常見的的排序算法能夠分兩大類:github

  1. 比較類排序:經過比較來決定元素的相對次序,因爲其時間複雜度沒法突破 O(nlogn),所以也叫作非線性時間排序。
  2. 非比較類排序:不是經過比較元素來決定元素的相對次序,能夠突破比較排序的時間下限,線性時間運行,也叫作線性時間非比較類排序。

經典算法

學會評估一個排序算法

學習算法,除了知道原理以及代碼實現之外,還有更重要的是學會如何評價、分析一個排序算法的 執行效率、內存損耗、穩定性。算法

執行效率

通常經過以下方面衡量:編程

1.最好、最壞、平均時間複雜度數組

爲什麼要區分這三種時間複雜度?第一,經過複雜度能夠大體判斷算法的執行次數。第二,對於要排序的數據有的無序、有的接近有序,有序度不一樣不一樣對於執行時間是不同的,因此咱們要只掉不一樣數據場景下算法的性能。編程語言

2. 時間複雜度的係數、常數、低階ide

咱們知道,時間複雜度反應的是數據規模 n 很大的時候的一個增加趨勢,因此它表示的時候會忽略係數、常數、低階。可是實際的軟件開發中,咱們排序的多是 10 個、100 個、1000 個這樣規模很小的數據,因此,在對同一階時間複雜度的排序算法性能對比的時候,咱們就要把係數、常數、低階也考慮進來。函數

3.比較次數移動(交換)數據次數
基於比較排序的算法執行過程都會涉及兩個操做、一個是比較,另外一個就是元素交換或者數據移動。因此咱們也要把數據交換或者移動次數考慮進來。性能

內存消耗

算法的內存消耗經過空間複雜度來衡量,不過在這裏針對排序算法的內存算好還有一個新概念,原地排序就是特指空間複雜度爲 O(1) 的算法,此次所講的算法都是原地排序算法。

算法的穩定性

若是待排序的序列中存在值相等的元素,通過排序以後,相等元素之間原有的前後順序不變。** 好比 a 本來在 b 前面,而 a=b ,排序以後 a 仍然在 b 的前面。

好比咱們有一組數據 2,9,3,4,8,3,按照大小排序以後就是 2,3,3,4,8,9。

這組數據裏有兩個 3。通過某種排序算法排序以後,若是兩個 3 的先後順序沒有改變,那咱們就把這種排序算法叫做穩定的排序算法;若是先後順序發生變化,那對應的排序算法就叫做不穩定的排序算法

冒泡排序

冒泡排序只會操做相鄰的兩個數據。每次冒泡操做都會對相鄰的兩個元素進行比較,看是否知足大小關係要求。若是不知足就讓它倆互換。一次冒泡會讓至少一個元素移動到它應該在的位置,重複 n 次,就完成了 n 個數據的排序工做。

這個算法的名字由來是由於越小的元素會經由交換慢慢「浮」到數列的頂端。

做爲最簡單的排序算法之一,冒泡排序給個人感受就像 Abandon 在單詞書裏出現的感受同樣,每次都在第一頁第一位,因此最熟悉。冒泡排序還有一種優化算法,就是立一個 flag,當在一趟序列遍歷中元素沒有發生交換,則證實該序列已經有序。但這種改進對於提高性能來講並無什麼太大做用。

算法步驟

  1. 比較相鄰的元素。若是第一個比第二個大,就交換他們兩個。

  2. 對每一對相鄰元素做一樣的工做,從開始第一對到結尾的最後一對。這步作完後,最後的元素會是最大的數。

  3. 針對全部的元素重複以上的步驟,除了最後一個。

  4. 持續每次對愈來愈少的元素重複上面的步驟,直到沒有任何一對數字須要比較。

    冒泡排序

/**
 * 冒泡排序: 時間複雜度 O(n²),最壞時間複雜度 O(n²),最好時間複雜度 O(n),平均時間複雜度 O(n²)
 * 空間複雜度 O(1),穩定排序算法
 */
public class BubbleSort implements ComparisonSort {
    @Override
    public int[] sort(int[] sourceArray) {
        // 複製數組,不改變參數內容
        int[] result = Arrays.copyOf(sourceArray, sourceArray.length);
        if (sourceArray.length <= 1) {
            return result;
        }
        int length = result.length;
        for (int i = 0; i < length; i++) {
            // 設定標記,當沒有數據須要交換的時候則說明已經有序,提早退出外部循環
            boolean hasChange = false;
            for (int j = 0; j < (length - 1) - i ; j++) {
                if (result[j] > result[j + 1]) {
                    // 數據交換
                    int temp = result[j];
                    result[j] = result[j + 1];
                    result[j + 1] = temp;
                    hasChange = true;
                }
            }
            if (!hasChange) {
                // 沒有數據交換,已經有序,提早退出
                break;
            }
        }
        return result;
    }
}

那麼問題來了,咱們來分析下這個算法的效率如何,教你們學會如何評估一個算法:

1.冒泡是原地排序算法麼?

由於冒泡的過程只有相鄰數據的交換操做,屬於常量級別的臨時空間,因此空間複雜度是 O(1),屬於原地排序算法。

2.是穩定排序算法?

只有交換才改變兩個元素的先後順序,當相鄰數據相等,不作交換,因此相同大小的數據在排序先後都不會改變順序,屬於穩定排序算法。

3.時間複雜度

最好時間複雜度:當數據已經有序,只須要一次冒泡,因此是 O(1)。(ps:都已是正序了,還要你冒泡何用)

最壞時間複雜度: 數據是倒序的,咱們須要進行 n 次冒泡操做,因此最壞狀況時間複雜度爲 O(n2)。(ps:寫一個 for 循環反序輸出數據不就好了,幹嗎要用你冒泡排序呢,我是閒的嗎)

插入排序

咱們先來看一個問題。一個有序的數組,咱們往裏面添加一個新的數據後,如何繼續保持數據有序呢?很簡單,咱們只要遍歷數組,找到數據應該插入的位置將其插入便可。

插入排序是一種最簡單直觀的排序算法,它的工做原理是經過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。

插入排序也包含兩種操做,一種是元素的比較,一種是元素的移動。當咱們須要將一個數據 a 插入到已排序區間時,須要拿 a 與已排序區間的元素依次比較大小,找到合適的插入位置。找到插入點以後,咱們還須要將插入點以後的元素順序日後移動一位,這樣才能騰出位置給元素 a 插入。

插入排序

代碼以下所示:

/**
 * 插入排序:時間複雜度 O(n²),平均時間複雜度 O(n²),最好時間複雜度 O(n),
 * 最壞時間複雜度 O(n²),空間時間複雜度 O(1).穩定排序算法。
 */
public class InsertionSort implements ComparisonSort {

    @Override
    public int[] sort(int[] sourceArray) {
        int[] result = Arrays.copyOf(sourceArray, sourceArray.length);
        if (sourceArray.length <= 1) {
            return result;
        }
        // 從下標爲 1 開始比較選擇合適位置插入,由於下標 0 只有一個元素,默認是有序
        int length = result.length;
        for (int i = 1; i < length; i++) {
            // 待插入數據
            int insertValue = result[i];
            // 從已排序的序列最右邊元素開始比較,找到比待插入樹更小的數位置
            int j = i - 1;
            for (; j >= 0; j--){
                if (result[j] > insertValue) {
                    // 向後移動數據,騰出待插入位置
                    result[j + 1] = result[j];
                } else {
                    // 找到待插入位置,跳出循環
                    break;
                }
            }
            // 插入數據,由於前面多執行了 j--,
            result[j + 1] = insertValue;
        }
        return result;
    }
}

依然繼續分析該算法的性能。

1.是不是原地排序算法

從實現過程就知道,插入排序不須要額外的存儲空間,因此空間複雜度是 O(1),屬於原地排序。

2.是不是穩定排序算法

對於值相等的元素,咱們選擇將數據插入到前面元素的侯娜,這樣就保證原有的先後順序不變,屬於穩定排序算法。

3.時間複雜度

若是要排序的數據已是有序的,咱們並不須要搬移任何數據。若是咱們從尾到頭在有序數據組裏面查找插入位置,每次只須要比較一個數據就能肯定插入的位置。因此這種狀況下,最好是時間複雜度爲 O(n)。注意,這裏是從尾到頭遍歷已經有序的數據

若是數組是倒序的,每次插入都至關於在數組的第一個位置插入新的數據,因此須要移動大量的數據,因此最壞狀況時間複雜度爲 O(n²)。

還記得咱們在數組中插入一個數據的平均時間複雜度是多少嗎?沒錯,是 O(n)。因此,對於插入排序來講,每次插入操做都至關於在數組中插入一個數據,循環執行 n 次插入操做,因此平均時間複雜度爲 O(n²)。

選擇排序

選擇排序是一種簡單直觀的排序算法,不管什麼數據進去都是 O(n²) 的時間複雜度。因此用到它的時候,數據規模越小越好。

選擇排序算法的實現思路有點相似插入排序,也分已排序區間和未排序區間。可是選擇排序每次會從未排序區間中找到最小的元素,將其放到已排序區間的末尾。

算法步驟

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
  2. 再從剩餘未排序元素中繼續尋找最小(大)元素,而後放到已排序序列的末尾。
  3. 重複第二步,直到全部元素均排序完畢。

選擇排序

代碼以下:

public class SelectionSort implements ComparisonSort {

    @Override
    public int[] sort(int[] sourceArray) {
        int length = sourceArray.length;
        int[] result = Arrays.copyOf(sourceArray, length);
        if (length <= 0) {
            return result;
        }
        // 一共須要 length - 1 輪比較
        for (int i = 0; i < length - 1; i++) {
            // 每輪須要比較的次數 length - i,找出最小元素下標
            int minIndex = i;
            for (int j = i + 1; j < length; j++) {
                if (result[j] < result[minIndex]) {
                    // 查出每次最小遠元素下標
                    minIndex = j;
                }
            }
            // 將當前 i 位置的數據與最小值交換數據
            if (i != minIndex) {
                int temp = result[i];
                result[i] = result[minIndex];
                result[minIndex] = temp;
            }
        }
        return result;
    }
}

首先,選擇排序空間複雜度爲 O(1),是一種原地排序算法。選擇排序的最好狀況時間複雜度、最壞狀況和平均狀況時間複雜度都爲 O(n²)。

那選擇排序是穩定的排序算法嗎?

答案是否認的,選擇排序是一種不穩定的排序算法。從我前面畫的那張圖中,你能夠看出來,選擇排序每次都要找剩餘未排序元素中的最小值,並和前面的元素交換位置,這樣破壞了穩定性

好比 5,8,5,2,9 這樣一組數據,使用選擇排序算法來排序的話,第一次找到最小元素 2,與第一個 5 交換位置,那第一個 5 和中間的 5 順序就變了,因此就不穩定了。正是所以,相對於冒泡排序和插入排序,選擇排序就稍微遜色了。

總結

這三種時間複雜度爲 O(n²) 的排序算法中,冒泡排序、選擇排序,可能就純粹停留在理論的層面了,學習的目的也只是爲了開拓思惟,實際開發中應用並很少,可是插入排序仍是挺有用的。後面講排序優化的時候,我會講到,有些編程語言中的排序函數的實現原理會用到插入排序算法。(希爾排序就是插入排序的一種優化)

今天講的這三種排序算法,實現代碼都很是簡單,對於小規模數據的排序,用起來很是高效。可是在大規模數據排序的時候,這個時間複雜度仍是稍微有點高,因此咱們更傾向於用下一節要講的時間複雜度爲 O(nlogn) 的排序算法。

算法執行效率

課後思考

最後給你們一個問題,答案可在後臺發送 「插入」獲取答案,也能夠加羣跟咱們一塊兒討論。

問題是:插入排序和冒泡排序時間複雜度相同,都是 O(n²),實際開發中更傾向於插入排序而不是冒泡排序

碼哥字節

相關文章
相關標籤/搜索