冒泡排序、插入排序、選擇排序

在平時的項目中,我們遇到最多的算法應該就是排序了。其中最經典、最常用的算法有:冒泡排序、插入排序、選擇排序、快速排序、歸併排序、基數排序等。

1. 評判排序算法的標準

排序算法有很多種,那麼我們該如何評判一個排序算法呢?一般情況下,我們可以從排序算法的執行效率、排序算法的內存消耗和排序算法的穩定性去考慮。

1.1 執行效率

1. 最好情況、最壞情況、平均情況時間複雜度

在要排序的數據中,執行效率與原始數據是否有序有關,有的原始數據接近有序,有的原始數據完全無序,對於有序度不同的數據,去排序執行的時間肯定有影響,因此我們有必要知道最好情況、最壞情況和平均情況時間複雜度下的執行效率。

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

通過前面的學習,我們知道:時間複雜度反應的是數據規模n趨向無窮大時的一個增長趨勢,在理想情況下我們會忽略係數、常數、低階。但在實際開發過程中,我們排序的可能是10個、100個、1000個這樣規模很小的數據,所以,在對同一階時間複雜度的排序算法性能對比的時候,我們需要將係數、常數、低階也考慮進去。

3. 比較次數和交換(或移動)次數

在基於比較排序算法的執行過程中,一般是進行比較元素大小或元素交換或移動。因此我們在分析排序算法的執行效率的時候,應該將比較次數和交換(移動)次數考慮進去。

1.2 內存消耗

算法的內存消耗可以用空間複雜度來衡量。在排序算法的空間複雜度中,有一個概念叫"原地排序":是指空間複雜度爲O(1)的排序算法。

1.3 穩定性

排序算法中的穩定性是指:在待排序的數據中存在相等的元素,經過排序後,相等元素之間原有的先後順序不變。

例如有如下一組數據:

2 6 3 8 9 3 7 

在上面的數據中有兩個3。

經過某種排序算法之後,如果兩個3的前後順序沒有改變,那麼這個排序算法就叫做"穩定的排序算法";如果前後順序發生變化,那麼對應的排序算法就叫做"不穩定的排序算法"。

2. 冒泡排序

  • 冒泡排序操作的是相鄰的兩個數據

  • 每次冒泡操作都會對相鄰的兩個元素進行比較,看看是否滿足大小關係要求,如果不滿足就讓它倆互換

  • 一次冒泡會讓至少一個元素移動到它應該在的位置,重複n次,就完成了n個數據的排序工作

舉例說明:

爲了更好的理解冒泡排序,這裏我們舉一個例子:對4,5,6,3,2,1這六個數字從小到大進行排序。

第一次冒泡操作的詳細過程如下:

在這裏插入圖片描述

通過上面的照片我們可以看到:經過第一次冒泡操作後,6這個元素已經存儲在正確的位置上了。

如果想要完成全部數據的排序,那麼我們就需要進行六次這樣的冒泡操作,每次冒泡操作後的如下圖所示:

在這裏插入圖片描述

實際上,在上面冒泡的過程中可以進行優化:當某次冒泡操作已經沒有數據交換時,說明已經達到完全有序了,就不用在繼續執行後續的冒泡操作。

舉例說明:

這裏給定6個元素,只需要進行4次冒泡操作就可以了,如下圖所示:

在這裏插入圖片描述

示例代碼:

結合前面的分析,我們可以得出冒泡排序的代碼實現:

// 冒泡排序,a 表示數組,n 表示數組大小
public void bubbleSort(int[] a, int n) {
  if (n <= 1) return;
 
 for (int i = 0; i < n; ++i) {
    // 提前退出冒泡循環的標誌位
    boolean flag = false;
    for (int j = 0; j < n - i - 1; ++j) {
      if (a[j] > a[j+1]) { // 交換
        int tmp = a[j];
        a[j] = a[j+1];
        a[j+1] = tmp;
        flag = true;  // 表示有數據交換      
      }
    }
    if (!flag) break;  // 沒有數據交換,提前退出
  }
}

結合前面的三個算法評判標準來看看冒泡排序:

1. 冒泡排序的時間複雜度?

最好情況:

如果要排序的數據已經是有序的,那我們只需要進行一次冒泡操作就行了,這是最好情況,此時的時間複雜度是O(n)。

最壞情況:

如果要排序的數據剛好是倒序的,那麼我們就需要進行n次冒泡操作,這是最壞情況,此時的時間複雜度是O(n^2)。

平均情況:

平均時間複雜度就是加權平均期望時間複雜度,分析的時候要結合概率論的知識。

對於包含n個數據的數組,這n個數據就有n階乘中排列方式。不同的排列方式,冒泡排序執行的時間肯定不同。

如果用概率論方法定量分析平均時間複雜度,涉及到數學推理和計算就會很複雜。不過有另外一種分析思路,通過“有序度”和“逆序度”這兩個概念來分析。

有序度是數組中具有有序關係的元素對的個數。

數學表達式如下:

有序元素對:a[i] <= a[j], 如果 i < j。

舉例說明:

在這裏插入圖片描述

同理,對於一個倒序排列的數組,比如6,5,4,3,2,1,有序度就是0;對於一個完全有序的數組,比如1,2,3,4,5,6,有序度就是n*(n-1)/2,也就是15。我們把這種完全有序的數組的有序度叫作 滿有序度

**逆序度:**是數組中不具有有序關係的元素對的個數。

逆序度的定義正好跟有序度相反(默認從小到大爲有序),數學表達式如下:

逆序元素對:a[i] > a[j], 如果 i < j。

上面講了有序度、滿有序度和逆序度的概念,它們之間有如下關係:

逆序度 = 滿有序度 - 有序度

其實,我們在排序的過程中就是一種增加有序度,減少逆有序度的過程,最後達到滿有序度,就說明排序完成了。

繼續前面冒泡排序的例子來說明:要排序的數組的初始狀態是4,5,6,3,2,1,其中有序元素對有(4,5)(4,6)(5,6),所以有序度是3。而n=6,所以排序完成之後終態的滿有序度爲n*(n-1)/2=15。

在這裏插入圖片描述

冒泡排序包含兩個操作原子,比較交換

每交換一次,有序度就加1。不管算法怎麼改進,交換次數總是確定的,即爲逆序度,也就是n*(n-1)/2 - 初始有序度。上面的例子就是15-3=12,即要進行12次交換操作。

對於包含n個數據的數組進行冒泡排序:

最壞情況下,初始狀態的有序度是0,所以要進行n*(n-1)/2次交換;

最好情況下,初始狀態的有序度是n*(n-1)/2,就不需要進行交換。

這裏我們可以取箇中間值n*(n-1)/4,來表示初始有序度既不是很高也不是很低的平均情況。此時就需要n*(n-1)/4次交換操作,比較操作肯定要比交換操作多,而複雜度的上限是O(n2),所以平均情況下的時間複雜度就是O(n2)。

這種計算平均複雜度的過程並不嚴格,但相比於概率論的定量分析還是比較實用的!

2. 冒泡排序的內存消耗如何?

冒泡過程只涉及相鄰數據的交換操作,只需要常量級的臨時空間,所以它的空間複雜度爲O(1),是一個原地排序算法。

3. 冒泡排序穩定嗎?

在冒泡排序中,只有交換纔可以改變兩個元素的前後順序。爲了保證冒泡排序算法的穩定性,當有相鄰的兩個元素大小相等的時候,我們不做交換,相同大小的數據在排序前後不會改變順序,所以冒泡排序是穩定的排序算法。

3. 插入排序

在插入排序中,我們將數組中的數據分爲兩個區間:已排序區間和未排序區間。初始已排序區間只有一個元素,就是數組中的第一個元素。

插入排序算法的核心思想就是取未排序區間中的元素,在已排序區間中找到合適的插入位置將其插入,並保證已排序區間數據一直有序。重複這個過程,直到未排序區間中元素爲空,算法結束。

舉例說明:

要排序的初始數據是4,5,6,1,3,2,其中左側爲排序區間,右側是未排序區間。

在這裏插入圖片描述

插入排序主要包含元素的比較和元素的移動兩種操作。當我們需要將一個數據a插入到已排序區間時,需要將a與已經排序區間的元素依次比較大小,找到合適的插入位置。找到插入點後,還需要將插入點之後的元素順序往後移動一位,這樣才能騰出位置給元素a插入。

對於不同的查找插入點方法(從頭到尾、從尾到頭),元素的比較次數是有區別的。但對於一個給定的初始序列,移動操作的次數是固定的,等於逆序度。

示例代碼:

// 插入排序,a 表示數組,n 表示數組大小
public void insertionSort(int[] a, int n) {
  if (n <= 1) return;

  for (int i = 1; i < n; ++i) {
    int value = a[i];
    int j = i - 1;
    // 查找插入的位置
    for (; j >= 0; --j) {
      if (a[j] > value) {
        a[j+1] = a[j];  // 數據移動
      } else {
        break;
      }
    }
    a[j+1] = value; // 插入數據
  }
}

結合前面的三個算法評判標準來看看插入排序:

1. 插入排序的時間複雜度?

最好情況:

如果要排序的數據已經是有序的,那麼我們就不需要移動任何數據。

如果在從尾到頭的有序數據中找插入位置,每次只需要比較一個數據就能確定插入的位置,這是最好的情況,此時的時間複雜度是O(n)。

最壞情況:

如果數組是倒序的,每次插入都相當於在數組的第一個位置插入新的數據,這是最壞的情況,此時的時間複雜度是O(n^2)。

平均情況:

前面我們說了在數組中插入一個數據的平均時間複雜度是O(n),所以,對於插入排序來說,每次插入操作相當於在數組中插入一個數據,循環執行n次插入操作,所以平均情況的時間複雜度是O(n^2)。

2. 插入排序的內存消耗如何?

從上面的示例代碼可以看出,插入排序算法的運行並不需要額外的存儲空間,所以空間複雜度是O(1),即插入排序是一個原地排序算法。

3. 插入排序穩定嗎?

在插入排序中,對於值相同的元素,可以選擇將後面出現的元素插入到前面出現元素的後面,這樣就可以保持原有的前後順序不變,所以插入排序是穩定的排序算法。

4. 選擇排序

選擇排序算法的實現思路也分已排序區間和未排序區間。選擇排序是每次都會從未排序區間中找到最小的元素,然後再將其放到已排序區間的末尾。

舉例說明:

要排序的初始數據是4,5,6,3,2,1,其中默認第一個數爲排序區間,右側其它的是未排序區間。

在這裏插入圖片描述

示例代碼:

// 選擇排序,a表示數組,n表示數組大小
public static void selectionSort(int[] a, int n) {
    if (n <= 1) return;

    for (int i = 0; i < n - 1; ++i) {
        // 查找最小值
        int minIndex = i;
        for (int j = i + 1; j < n; ++j) {
            if (a[j] < a[minIndex]) {
                minIndex = j;
            }
        }

        // 交換
        int tmp = a[i];
        a[i] = a[minIndex];
        a[minIndex] = tmp;
    }
}

結合前面的三個算法評判標準來看看插入排序:

1. 選擇排序的時間複雜度?

選擇排序的最好情況、最壞情況和平均情況的時間複雜度都是O(n^2)。

2. 選擇排序的內存消耗如何?

選擇排序的運行並不需要額外的存儲空間,所以空間複雜度是O(1),即選擇排序是一個原地排序算法。

3. 選擇排序穩定嗎?

選擇排序每次都要從未排序元素中找最小值,並和前面的元素交換位置,這樣就是破壞了穩定性。

例如5,8,5,2,9這樣一組數據,使用選擇排序算法排序的時候,第一次找到最小元素2,與第一個5交換位置,那第一個5和中間的5順序就變了。所以選擇排序不穩定。

非常感謝您的耐心閱讀,希望我的文章對您有幫助。歡迎點評、轉發或分享給您的朋友或技術羣。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章