1. 程式人生 > >排序演算法上——氣泡排序、插入排序和選擇排序

排序演算法上——氣泡排序、插入排序和選擇排序

1. 排序演算法?

排序演算法應該算是我們最熟悉的演算法了,我們學的第一個演算法,可能就是排序演算法,而在實際應用中,排序演算法也經常會被用到,其重要作用不言而喻。

經典的排序演算法有:氣泡排序、插入排序、選擇排序、歸併排序、快速排序、計數排序、基數排序、桶排序。按照時間複雜度,可以分為以下三類。

排序演算法


2. 如何分析一個排序演算法?

2.1. 排序演算法的執行效率

  • 最好情況、最壞情況、平均情況時間複雜度。我們不僅要知道一個排序演算法的最好情況、最壞情況、平均情況時間複雜度,還要知道它們分別對應的原始資料是什麼樣的。有序度不同的資料,對於排序演算法的執行時間肯定是有影響的。

  • 時間複雜度的係數、常量、低階。時間複雜度反映的是資料規模非常大時的一個增長趨勢,但實際中,我們面臨的資料可能是 10 個、100 個、 1000 個這樣的小資料,因此之前我們忽略的係數、常量、低階也要考慮進來。

  • 比較次數和交換(或移動)次數。基於比較的排序演算法涉及到兩個主要操作,一個是元素比較大小,另一個是元素交換或移動。

2.2. 排序演算法的記憶體消耗

  • 演算法的記憶體消耗可以用空間複雜度來衡量,針對排序演算法的空間複雜度,我們引入了一個新的概念,原地排序(Sorted in place),特指空間複雜度為 O
    ( 1 ) O(1)
    的排序演算法,即指直接在原有資料結構上進行排序,無需額外的記憶體消耗。

2.3. 排序演算法的穩定性

  • 排序演算法的穩定性指的是,如果待排序的序列中存在值相等的資料,經過排序之後,相等元素之間原有的先後循序不變。

  • 比如一組資料 2, 9, 3, 4, 8, 3,按照大小排序之後就是 2, 3, 3, 4, 8, 9,如果排序後兩個 3 的順序沒有發生改變,我們就把這種演算法叫作穩定的排序演算法

    ,反之就叫作不穩定的排序演算法

  • 我們學習排序的時候一般都是用整數來舉例,但在真正的軟體開發中,待排序的資料往往不是單純的整數,而是一組物件,我們需要按照物件的某一個鍵值對資料進行排序

  • 假設我們有 10 萬條訂單資料,要求金額從小到大排序,並且金額相同的訂單按照下單時間從早到晚排序。這時候,穩定排序就發揮了其作用,我們可以先按照下單時間對資料進行排序,然後再用穩定排序演算法按照訂單金額重新排序

穩定排序演算法


3. 氣泡排序(Bubble Sort)?

3.1. 氣泡排序演算法實現

  • 氣泡排序只會操作相鄰的兩個資料,將其調整到正確的順序。一次冒泡會讓至少一個數據移動到它應該在的位置,冒泡 n 次,就完成了 n 個數據的排序工作。

氣泡排序

氣泡排序

  • 若某一次冒泡沒有資料移動,則說明資料已經完全達到有序,不用再繼續執行後續的冒泡操作,針對此我們可以再對剛才的演算法進行優化。

氣泡排序

  • 程式碼實現
// O(n^2)
void Bubble_Sort(float data[], int n)
{
    int i = 0, j = 0;
    int temp = 0;
    int flag = 0;
    for(i = n-1; i > 0; i--)
    {
        flag = 0;
        for(j = 0; j < i; j++)
        {
            if(data[j+1] < data[j])
            {
                temp = data[j];
                data[j] = data[j+1];
                data[j+1] = temp;
                flag = 1;
            }
        }

        if(!flag)//If no data needs to be exchanged, the sort finishes.
        {
            break;
        }
    }
}

3.2. 氣泡排序演算法分析

  • 氣泡排序是一個原地排序演算法,只需要常量級的臨時空間。

  • 氣泡排序是一個穩定的排序演算法,當元素大小相等時,我們沒有進行交換。

  • 最好情況下,資料已經是有序的,我們只需要進行一次冒泡即可,時間複雜度為 O ( n ) O(n) 。最壞情況下,資料剛好是倒序的,我們需要進行 n 次冒泡,時間複雜度為 O ( n 2 ) O(n^2)

3.3. 有序度和逆序度

  • 有序度是陣列中具有有序關係的元素對的個數。有序元素對定義:a[i] <= a[j], 如果 i < j。

有序度

  • 完全倒序排列的陣列,其有序度為 0;完全有序的陣列,其有序度為 C n 2 = n ( n 1 ) 2 C_n^2 = \frac{n*(n-1)}{2} ,我們把這種完全有序的陣列叫作滿有序度

  • 逆序度和有序度正好相反,逆序元素對定義:a[i] > a[j], 如果 i < j。

  • 逆序度 = 滿有序度 - 有序度。排序的過程就是一個增加有序度減少逆序度的過程,最後達到滿有序度,排序就完成了。

  • 在氣泡排序中,每進行一次交換,有序度就加 1。不管演算法怎麼改進,交換次數總是確定的,即為逆序度。

  • 最好情況下,需要的交換次數為 0;最壞情況下,需要的交換次數為 n ( n 1 ) 2 \frac{n * (n-1)}{2} 。平均情況下,需要的交換次數為 n ( n 1 ) 4 \frac{n * (n-1)}{4} ,而比較次數肯定要比交換次數多,而複雜度的上限是 O ( n 2 ) O(n^2) ,所以,平均時間複雜度也就是 O ( n 2 ) O(n^2)


4. 插入排序(Insertion Sort)

4.1. 插入排序演算法實現

  • 往一個有序的陣列插入一個新的元素時,我們只需要找到新元素正確的位置,就可以保證插入後的陣列依然是有序的。
    插入元素

  • 插入排序就是從第一個元素開始,把當前資料的左側看作是有序的,然後將當前元素插入到正確的位置,依次往後進行,直到最後一個元素。

  • 對於不同的查詢插入點方法(從頭到尾、從尾到頭),元素的比較次數是有區別的,但移動次數是確定的就等於逆序度

  • 程式碼實現

// O(n^2)
void Insertion_Sort(float data[], int n)
{
    int i = 0, j = 0;
    int temp = 0;
    for(i = 1; i < n; i++)
    {
        temp = data[i];
        for(j = i; j > 0; j--)
        {
            if(temp < data[j-1])
            {
                data[j] = data[j-1];
            }
            else// The data ahead has been sorted correctly.
            {
                break;
            }
        }
        data[j] = temp; // insert the data
    }
}

4.2. 插入排序演算法分析

  • 插入排序是一個原地排序演算法,只需要常量級的臨時空間。

  • 插入排序是一個穩定的排序演算法,當元素大小相等時,我們不進行插入。

  • 最好情況下,資料已經是有序的,從尾到頭進行比較的話,每次我們只需要進行一次比較即可,時間複雜度為 O ( n ) O(n) 。最壞情況下,資料剛好是倒序的,我們每次都要在陣列的第一個位置插入新資料,有大量的移動操作,時間複雜度為 O ( n 2 ) O(n^2)

  • 在陣列中插入一個元素的平均時間複雜度為 O ( n ) O(n) ,這裡,我們需要迴圈執行 n 次插入操作,所以平均時間複雜度為 O ( n 2 ) O(n^2)


5. 選擇排序(Selection Sort)

5.1. 選擇排序演算法實現

  • 選擇排序就是從第一個元素開始,從當前資料的右側未排序區間中選取一個最小的元素,然後放到左側已排序區間末尾,依次往後進行,直到最後一個元素。
    選擇排序

  • 程式碼實現

// O(n^2)
void Selection_Sort(float data[], int n)
{
    int i = 0, j = 0, k = 0;
    int temp = 0;
    for(i = 0; i < n-1; i++)
    {
        k = i;
        for(j = i+1; j < n; j++)
        {
            if(data[j] < data[k])
            {
                k = j;
            }
        }
        if(k != i)
        {
            temp = data[i];
            data[i] = data[k];
            data[k] = temp;
        }
    }
}

5.2. 選擇排序演算法分析

  • 選擇排序是一個原地排序演算法,只需要常量級的臨時空間。

  • 選擇排序的最好情況時間複雜度、最壞情況時間複雜度和平均情況時間複雜度都為 O ( n 2 ) O(n^2) ,因為不管資料排列情況怎樣,都要進行相同次數的比較

  • 選擇排序是一個不穩定的排序演算法,因為每次都要從右側未排序區間選擇一個最小值與前面元素交換,這種交換會打破相等元素的原始位置。


6. 為什麼插入排序比氣泡排序更受歡迎?

交換排序和插入排序的時間複雜度都為 O ( n 2 ) O(n^2) ,也都是原地排序演算法,為什麼插入排序更受歡迎呢?

  • 前面分析,插入排序的移動次數等於逆序度,氣泡排序的交換次數等於逆序度,但氣泡排序每次交換需要進行三次賦值操作,而插入排序每次移動只需要一次賦值操作,其相應的真實執行時間也會更短。
氣泡排序中資料的交換操作:
if (a[j] > a[j+1]) { // 交換
   int tmp = a[j];
   a[j] = a[j+1];
   a[j+1] = tmp;
   flag = true;
}

插入排序中資料的移動操作:
if (a[j] > value) {
  a[j+1] = a[j];  // 資料移動
} else {
  break;
}

7. 小結

三種排序演算法對比

  • 氣泡排序和選擇排序在實際中應用很少,僅僅停留在理論層次即可,選擇排序演算法還是挺有用的,而且其還有很大的優化空間,比如希爾排序。

參考資料-極客時間專欄《資料結構與演算法之美》

獲取更多精彩,請關注「seniusen」!
seniusen