Java 排序算法 - 為什么插入排序要比冒泡排序更受歡迎呢


Java 排序算法 - 為什么插入排序要比冒泡排序更受歡迎呢

數據結構與算法之美目錄(https://www.cnblogs.com/binarylei/p/10115867.html)

對於大多數程序員來說,我們學習的第一個算法,可能就是排序。大部分編程語言中,也都提供了排序函數。在平常的項目中,我們也經常會用到排序。排序算法太多了,有很多可能連名字都沒聽說過,比如猴子排序、睡眠排序、面條排序等。

這里我們只需要掌握最經典的、最常用的排序算法:冒泡排序、插入排序、選擇排序、歸並排序、快速排序、計數排序、基數排序、桶排序。我按照時間復雜度把它們分成了三類:

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

本文重點分析時間復雜度為 O(n2) 的三種算法:冒泡、選擇、插入排序,其中重點需要掌握的是插入排序,其它兩種基本的算法在實際軟件開發中根本不會使用到。

1. 衡量排序算法的三個指標

1.1 時間復雜度

(1)最好情況、最壞情況、平均情況時間復雜度。

為什么要區分這三種時間復雜度呢?第一,有些排序算法會區分,為了好對比,所以我們最好都做一下區分。第二,對於要排序的數據,有的接近有序,有的完全無序。有序度不同的數據,對於排序的執行時間肯定是有影響的,我們要知道排序算法在不同數據下的性能表現。

(2)時間復雜度的系數、常數 、低階。

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

(3)比較次數和交換次數。

基於比較的排序算法的執行過程,會涉及兩種操作,一種是元素比較大小,另一種是元素交換或移動。所以,如果我們在分析排序算法的執行效率的時候,應該把比較次數和交換(或移動)次數也考慮進去。

1.2 空間復雜度

針對排序算法的空間復雜度,我們還引入了一個新的概念,原地排序(Sorted in place)。原地排序算法,就是特指空間復雜度是 O(1) 的排序算法。

1.3 穩定性

針對排序算法還有另一個重要指標:穩定性。穩定性是指,排序后相等元素之間原有的先后順序不變。

比如我們有一組數據 2,9,3,4,8,3,按照大小排序之后就是 2,3,3,4,8,9。這組數據里有兩個 3。經過某種排序算法排序之后,如果兩個 3 的前后順序沒有改變,那我們就把這種排序算法叫作穩定的排序算法;如果前后順序發生變化,那對應的排序算法就叫作不穩定的排序算法

穩定的排序算法會什么用呢?比如我們需要針對訂單的金額和時間兩個維度排序(order by price, timestamp)。最先想到的方法是:我們先按照金額對訂單數據進行排序,然后,再遍歷排序之后的訂單數據,對於每個金額相同的小區間再按照下單時間排序。這種排序思路理解起來不難,但是實現起來會很復雜。

借助穩定排序算法,可以非常簡潔地解決多維度排序。解決思路是這樣的:我們先按照下單時間給訂單排序,再用穩定排序算法,按照訂單金額重新排序。兩遍排序之后,我們得到的訂單數據就是按照金額從小到大排序,金額相同的訂單按照下單時間從早到晚排序的。

1.4 常用排序算法比較

常用排序算法比較
排序算法 時間復雜度 原地算法 穩定性 備注
冒泡排序 O(n2)
選擇排序 O(n2) ×
插入排序 O(n2)
希爾排序 O(n1.5) × 插入排序的優化
歸並排序 O(nlogn)
快速排序 O(nlogn) ×
桶排序 O(n)
基數排序 O(n)
計數排序

2. 冒泡排序

冒泡排序依次對相鄰的元素進行比較並交換,從而實現排序功能。冒泡排序是復雜度為 O(n2) 的原地穩定排序算法。事實上,實際項目中不會使用冒泡排序,冒泡排序僅僅停留在理論階段。

2.1 冒泡排序原理分析

冒泡排序重復地走訪過要排序的元素列,依次比較兩個相鄰的元素,如果他們的順序錯誤就把他們交換過來。走訪元素的工作是重復地進行直到沒有相鄰元素需要交換,也就是說該元素已經排序完成。

冒泡次數 冒泡結果
原始數據(n = 6) 3 8 6 5 2 1
第一輪第1次冒泡:3 vs 8 3    8 6 5 2 1
第一輪第2次冒泡:8 vs 6 3 6    8 5 2 1
第一輪第3次冒泡:8 vs 5 3 6 5    8 2 1
第一輪第4次冒泡:8 vs 2 3 6 5 2    8 1
第一輪第5次冒泡:8 vs 1 3 6 5 2 1    8
第二輪第1次冒泡:3 vs 6 3    6 5 2 1 8
第二輪第2次冒泡:6 vs 5 3 5    6 2 1 8
第二輪第3次冒泡:6 vs 2 3 5 2    6 1 8
第二輪第4次冒泡:6 vs 1 3 5 6 1    6 8
...
第 n - 1 輪 1 2 3 5 6 8

說明: 冒泡比較過程如下:

  • 外層輪詢,相當於每輪詢一次都將最大的元素搬移到最后面。第一輪搬移最大元素到第 n - 1 個位置,第二輪搬移最大元素到第 n - 2 個位置,...,全部搬移完則需要搬移 n - 1 次。
  • 內層輪詢,比較前后兩個元素大小,如果位置順序錯誤就交換位置。已經搬移過的最大值,則不再需要比較。
for (int i = 0; i < arr.length; i++) {
    boolean swap = false;   // 一次都沒有交換,說明完全有序,直接return
    // 每次輪詢依次比較相鄰的兩個數,將最小的移到前面
    for (int j = 0; j < arr.length - i - 1; j++) {
        if (arr[j] > arr[j + 1]) {
            swap = true;
            int tmp = arr[j];
            arr[j] = arr[j + 1];
            arr[j + 1] = tmp;
        }
    }
    if (!swap) return;
}

2.2 冒泡排序三大指標分析

  1. 時間復雜度:數據完全有序,最好時間復雜度為 O(n)。數據完全逆序,最壞時間復雜度為 O(n2)。我們再思考一下平均復雜度的計算?這里引入有序度的概念。

    • 有序度:數組中有序元素對的個數。如 5, 1, 2 中 (1, 2) 是有序的,有序度為 1。
    • 逆序度:數據中無序元素對的個數。如 5, 1, 2 中 (5, 1) 和 (5, 2) 是無序的,逆序度為 2。
    • 滿有序度:固定為 n(n - 1) / 2,且 滿有序度 = 有序度 + 逆序度。

    冒泡排序中,一個逆序度就要進行一次交換,最好情況交換 0 次,最壞情況交換 n(n - 1) / 2 次,平均交換 n(n - 1) / 4 次。而比較操作肯定要比交換操作多,而復雜度的上限是 O(n2),所以平均情況下的時間復雜度就是 O(n2)

  2. 空間復雜度:使用原來的數組進行排序,是原地排序算法,即 O(1)。

  3. 穩定性:冒泡排序可以控制相鄰元素值相等時不交換位置,也就是元素值相等時順序不變,因此是穩定算法

3. 選擇排序

選擇排序相對於冒泡排序而言,每一輪只交換一次來提高排序效率。選擇排序是復雜度為 O(n2) 的原地非穩定排序算法。事實上,同冒泡排序一樣,實際項目中不會使用選擇排序,選擇排序僅僅停留在理論階段。

3.1 選擇排序原理分析

選擇排序同樣也基於比較算法實現的。和冒泡排序一樣,也分為兩層循環,外層循環每輪訓一次,就排序好一個數,因此外層同樣需要循環 n - 1 次。不同的是,外層循環每輪訓一次時只交換一次元素位置。

選擇排序 排序結果
原始數據(n = 6) 3 8 6 5 2 1
第一輪交換:8 vs 1 3 1 6 5 2 8
第二輪交換:6 vs 2 3 1 2 5 6 8
...
第 n - 1 輪 1 2 3 5 6 8

說明: 選擇排序先找出最大值才進行交換,不像冒泡排序都是相鄰元素比較后進行交換。但也正是因為選擇排序不是相鄰元素之間比較后交換,而是非相鄰元素間的交換,可能會導致相等的元素在排序后亂序,是非穩定排序。

public void sort(Integer[] arr) {
    for (int i = 0; i < arr.length; i++) {
        // 1. 比較找到最大值的位置,然后和最后一位進行交換
        int maxIndex = 0;
        int lastIndex = arr.length - i - 1;
        for (int j = 1; j < arr.length - i; j++) {
            if (arr[maxIndex] < arr[j]) {
                maxIndex = j;
            }
        }
        // 2. 交換
        if (maxIndex != lastIndex) {
            int tmpValue = arr[maxIndex];
            arr[maxIndex] = arr[lastIndex];
            arr[lastIndex] = tmpValue;
        }
    }
}

3.2 選擇排序三大指標分析

  1. 時間復雜度:最好情況時間復雜度、最壞情況和平均情況時間復雜度都為 O(n2)
  2. 空間復雜度:使用原來的數組進行排序,是原地排序算法,即 O(1)。
  3. 穩定性:選擇排序是非相鄰元素間交換,也就可能導致元素值相等時順序變化,因此是非穩定算法

4. 插入排序

插入排序原理在有序數組中插入一個元素仍是有序的。選擇排序是復雜度為 O(n2) 的原地穩定排序算法。插入排序是真正在項目中使用的排序算法,需要我們重點學習。

4.1 插入排序原理分析

插入排序將數組分為兩部分,有序部分和無序部分,依次遍歷無序部分將元素插入到有序數組中,最終達到有序。

冒泡次數 冒泡結果
原始數據(n = 6) 3 8 6 5 2 1
第一輪插入:將 a[1]=8 插入 a[0] 3 8 6 5 2 1
第二輪插入:將 a[2]=6 插入 a[0] ~ a[1] 3 6 8 5 2 1
...
第 n - 1 輪:將 a[n - 1]=1 插入 a[0] ~ a[n - 2] 1 2 3 5 6 8

說明: 插入排序每次都將 將 a[n - 1]=1 插入有序數組 a[0] ~ a[n - 2] 中,保證插入后的數據仍是有序的。

public void sort(Integer[] arr) {
    for (int i = 1; i < arr.length; i++) {
        int tmp = arr[i];
        int j;
        for (j = i; j > 0 && arr[j - 1] > tmp; j--) {
            arr[j] = arr[j - 1];
        }
        arr[j] = tmp;
    }
}

4.2 插入排序三大指標分析

  1. 時間復雜度:數據完全有序,最好時間復雜度為 O(n)。數據完全逆序,最壞時間復雜度為 O(n2)。由於往數組中插入一個元素的平均復雜度是 O(n),因此插入排序的平均復雜度也是 O(n2)。
  2. 空間復雜度:使用原來的數組進行排序,是原地排序算法,即 O(1)。
  3. 穩定性:插入排序也是相鄰元素間交換,也就是元素值相等時順序不變,,因此是穩定算法

5. 希爾排序

希爾排序是希爾(Donald Shell)於 1959 年提出的一種排序算法。希爾排序也是一種插入排序,它是簡單插入排序經過改進之后的一個更高效的版本,也稱為縮小增量排序,同時該算法是沖破 O(n2) 的第一批算法之一。

5.1 希爾排序原理分析

希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至 1 時,整個文件恰被分成一組,算法便終止。

當序列很長時,向前插入一次導致的移動操作可能非常大。如果原序列的無序程度較高,則能夠緩解這個問題,即小的數大致在前,大的數大致在后。

public void sort(Integer[] arr) {
    for (int gap = arr.length / 2; gap > 0; gap /= 2) {
        for (int i = gap; i < arr.length; i++) {
            int tmp = arr[i];
            int j;
            for (j = i; j >= gap && arr[j - gap] > tmp; j -= gap) {
                arr[j] = arr[j - gap];
            }
            arr[j] = tmp;
        }
    }
}

4.2 插入排序三大指標分析

  1. 時間復雜度:希爾排序時間復雜度分析很復雜,就不具體分析了。希爾排序中對於增量序列的選擇十分重要,直接影響到希爾排序的性能。我們上面選擇的增量序列 {n/2, (n/4) ... 1}(希爾增量),其最壞時間復雜度依然為 O(n2),一些經過優化的增量序列如 Hibbard 經過復雜證明可使得最壞時間復雜度為 O(n1.5)。
  2. 空間復雜度:實際使用的是插入排序,也是原地排序算法,即 O(1)。
  3. 穩定性:希爾排序增加了不同步長之間的排序,也就是非相鄰元素之間會交換,這樣也導致了元素值相等時順序會發生變化,,因此是不穩定算法

6. 總結:為什么插入排序要比冒泡排序更受歡迎呢?

我們前面分析冒泡排序和插入排序的時候講到,冒泡排序不管怎么優化,元素交換的次數是一個固定值,是原始數據的逆序度。插入排序是同樣的,不管怎么優化,元素移動的次數也等於原始數據的逆序度。雖然插入排序和冒泡排序元素交換的最終次數都是一樣的,但:

  • 冒泡排序元素交換比插入排序要復雜。插入排序只需要一次操作。
  • 插入排序的時間復雜度更穩定。冒泡排序只有當數據完全有序時,時間復雜度才是 O(1),只要有一個逆序度則時間復雜度就變成 O(n2)。而插入排序在數據比較有序時更有優勢。

參考:

  1. 排序動畫演示:http://www.jsons.cn/sort/

每天用心記錄一點點。內容也許不重要,但習慣很重要!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM