【小小前端】前端排序演算法第一期(氣泡排序、選擇排序、插入排序)

.Ping發表於2020-03-05

演算法分類

十種常見排序演算法可以分為兩大類:

非線性時間比較類排序:通過比較來決定元素間的相對次序,由於其時間複雜度不能突破O(nlogn),因此稱為非線性時間比較類排序。

線性時間非比較類排序:不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間執行,因此稱為線性時間非比較類排序。

【小小前端】前端排序演算法第一期(氣泡排序、選擇排序、插入排序)

演算法複雜度

【小小前端】前端排序演算法第一期(氣泡排序、選擇排序、插入排序)

相關概念

穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面。

不穩定:如果a原本在b的前面,而a=b,排序之後 a 可能會出現在 b 的後面。

時間複雜度:對排序資料的總的操作次數。反映當n變化時,操作次數呈現什麼規律。

空間複雜度:是指演算法在計算機內執行時所需儲存空間的度量,它也是資料規模n的函式。

氣泡排序(Bubble Sort)


原理:

比較兩個相鄰的元素,將值大的元素交換到右邊

演算法描述

  1. 比較第一位與第二位,如果第一位比第二位大,則交換位置
  2. 繼續比較後面的數,按照同樣的方法進行比較,到最後一位的時候,最大的數將被排在最後一位
  3. 重複進行比較,直到排序完成,注意由於上一次排序使得最後一位已經是最大的數,所以每次排序結束之後,下一次比較的時候可以相應的減少比較數量

動圖演示

【小小前端】前端排序演算法第一期(氣泡排序、選擇排序、插入排序)

程式碼解析

    let arr = [3, 45, 16, 8, 65, 15, 36, 22, 19, 1, 96, 12, 56, 12, 45];
    let flag;
    let len = arr.length;
    let num1 = 0; // 比較的次數
    let num2 = 0; // 交換的次數
    // 有15個數,只需要選出14個最大的數,最後一個數就是最小的,不用進行比較
    for(let i = 0; i < len -1 ; i++){
        // 每次i變化之後最大的值已經排序到最後一位,無需對最後一位進行比較,所以j的最大值為len-i-1
        for(let j = 0; j < len - i - 1; j++){
            num1 += 1;
            // 如果當前位置的數比下一位置的數大,則交換位置
            if(arr[j] > arr[j+1]){
                num2 =+ 1;
                flag = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = flag
           }
        }
    }
    console.log(arr,num);
複製程式碼

輸出的結果為:

【小小前端】前端排序演算法第一期(氣泡排序、選擇排序、插入排序)

計數器num1和num2的值分別為:105和46

從程式碼中看出,排序過程中,所需要的臨時變數一直都沒有變化,因此空間複雜度為O(1);程式碼進行了兩次for迴圈且是巢狀迴圈,因此時間複雜度為O(n²)。

氣泡排序的最優情況是原陣列預設正序排序,此時比較的次數num1仍為105,而交換次數num2為0,此時的時間複雜度仍然為O(n²),那麼為什麼前面的複雜度表格中說是O(n)呢?經過一番研究發現,需要對上述程式碼進行簡單優化。

【小小前端】前端排序演算法第一期(氣泡排序、選擇排序、插入排序)

如果排序的陣列是:[1,2,3,4,5],此時完全符合最優複雜度情況,當我們進行第一次迴圈發現,兩兩相鄰的資料一次都沒有進行交換,也就是說所有的數都比前一個數大,此時就是正序,無需再進行下次排序,所以我們只需要加上一個變數進行判斷:

    // 初始未產生交換
    let isSwap = false;
    for(let i = 0; i < len -1 ; i++){
        // 每次i變化之後最大的值已經排序到最後一位,無需對最後一位進行比較,所以j的最大值為len-i-1
        for(let j = 0; j < len - i - 1; j++){
            num1 += 1;
            // 如果當前位置的數比下一位置的數大,則交換位置
            if(arr[j] > arr[j+1]){
                num2 =+ 1;
                isSwap = true;
                flag = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = flag
           }
        }
        // 如果產生交換,直接結束迴圈
        if(!isSwap){
            return
        }
    }
複製程式碼

當產生交換時,isSwap變成true,第一次迴圈結束之後,如果isSwap如果還是false表示未經過交換,陣列已經是正序,無需繼續排序,此時的時間複雜度為O(n)

在比較過程中只判斷了大於後一個數,如果兩個數相等無需交換,所以氣泡排序是穩定的排序。

選擇排序(Selection Sort)


原理

將序列分為未排序和已排序,從未排序序列中找到最小的數,放到無序序列起始位置,然後繼續從剩餘未排序序列中繼續尋找最小值

演算法描述

  1. 初始無序序列為待排序序列,有序序列為空
  2. 從無序序列中找到最小值,放到無序序列起始位置,也就是和起始位置交換
  3. 將無序序列起始位向後推一個位置,繼續2步驟

動圖演示

【小小前端】前端排序演算法第一期(氣泡排序、選擇排序、插入排序)

程式碼解析

        // 選擇出無序序列中最小的值放到無序第一位
        let arr = [3, 45, 16, 8, 65, 15, 36, 22, 19, 1, 96, 12, 56, 12, 45];
        let len = arr.length;
        let minIndex;
        let flag;
        let num1 = 0; // 比較次數
        let num2 = 0; // 交換次數
        for(let i = 0; i < len - 1;i++){
            // 每次選擇最小值之後,無序區的開始位置往後推1
            minIndex = i;
            // j迴圈到最後一位,選擇出當前無序陣列中數值最小的索引值
            for(let j = i + 1; j < len;j++){
                num1 += 1;
                if(arr[minIndex]>arr[j]){
                    minIndex = j;
                }
            }
            num2 += 1;
            flag = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = flag;
            
        }
        console.log(arr,num1,num2)
複製程式碼

輸出結果為:

【小小前端】前端排序演算法第一期(氣泡排序、選擇排序、插入排序)

計數器num1和num2的值分別為:105和14

也就是說,選擇排序的比較次數和氣泡排序未優化時一樣,而交換的次數只有14次

再舉剛剛的栗子:[1,2,3,4,5],正序序列,無需進行任何交換,我們對最後交換程式碼進行優化:

if(minIndex == i){
    num2 += 1;
    flag = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = flag;
}
複製程式碼

此時沒有發生資料交換。

其實從程式碼中可以看到,無論如何,選擇排序總會經過N^2/2次比較,而受原始數列影響,交換的次數最大為n-1,最小次數為0。

因此選擇排序時間複雜度總為:O(n平方),空間複雜度為:O(1)

選擇排序是不穩定的,為什麼這麼說,看個栗子:[5,3,8,5,2],好了不說也能看出來了。

插入排序(Insertion Sort)


原理

構建有序序列,對於未排序的資料,從有序序列後向前掃描,找到相應位置插入

動圖演示

【小小前端】前端排序演算法第一期(氣泡排序、選擇排序、插入排序)

程式碼實現

        let arr = [3, 45, 16, 8, 65, 15, 36, 22, 19, 1, 96, 12, 56, 12, 45];
        let len = arr.length;
        // 定義當前未排序資料起始位置值也就是即將插入的資料
        let currentValue;
        // 有序序列遍歷位置
        let preIndex;
        let num1 = 0; // 比較次數
        let num2 = 0; // 交換次數
        for(let i = 1;i < len; i++){
            // 定義原始資料第二位為未排序資料第一位,預設原始資料第一位已排序
            currentValue = arr[i];
            // 當前有序序列最大索引
            preIndex = i - 1;
            // 當索引大於等於0且當前索引值大於需要插入的資料時
            for(let j = preIndex;j>=0;j--){
                // 第一次比較,如果有序序列最大索引值其實就是待插入資料前一位,比待插入資料大,則後移一位
                num1+=1;
                if(arr[preIndex]>currentValue){
                    arr[preIndex+1] = arr[preIndex];
                    preIndex --;
                    // 索引減1,繼續向前比較
                }
            }
            // 當出現索引所在位置值比待插入資料小時,將待插入資料插入
            // 為什麼是preIndex+1,因為在while迴圈裡面後移一位之後,當前索引已經變化
            num2 += 1;
            arr[preIndex+1] = currentValue;
        }
        console.log(...arr,num1,num2)
複製程式碼

輸出的結果為:

【小小前端】前端排序演算法第一期(氣泡排序、選擇排序、插入排序)

計數器num1和num2的值分別為:105和14

插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,為最新元素提供插入空間。

在時間複雜度上,如果按照上述寫法,執行的次數依次為,1,2,3....n-1,因此時間複雜度為O(n²)

【小小前端】前端排序演算法第一期(氣泡排序、選擇排序、插入排序)

氣泡排序出現了優化之後最優複雜度變小的情況,看看前面的表格,插入排序的最優時間複雜度為O(n),那麼這又是什麼原因呢?

我們看看判斷待插入值是否小於當前索引位置值的地方,用了一個for迴圈和一個if,我們能不能改寫一下呢?

        while(preIndex>=0&&arr[preIndex]>currentValue){
            arr[preIndex+1] = arr[preIndex];
            preIndex --;
        }
複製程式碼

這樣看,如果在正序情況下:[1,2,3,4,5],每次比較都不會進入while迴圈,因此只執行了n-1次比較操作,因此此時時間複雜度為O(n),那麼最壞複雜度其實也就是逆序情況了,需要執行1,2,3...n次,因此最壞時間複雜度為O(n²)。

總結

綜合比較一下最簡單的三種排序方法:

選擇排序在氣泡排序上做了優化,氣泡排序兩兩比較每一輪選出一個最大值,而選擇排序則從序列中直接選擇出最小值插入無序序列首部(進行交換),相對於氣泡排序減少了不必要的換位操作。

插入排序在思想上和選擇排序差不多,選擇排序是從無序序列中找出最小值,與無序序列的首位進行交換,從而生成一個有序序列,而插入排序則從無序序列中直接選出首位,將首位與有序序列進行比較,插入相應的位置。

使用場景

對於一般工作來說,這三種使用沒什麼體驗上的差距

如果非要選擇的話,插入排序比選擇排序少一些比較的次數,但選擇排序有時候比插入排序少挪動次數,建議資料較大時用插入排序,資料量較小時可以用選擇排序。(其實這倆差不多--)

參考

新手上路請多指教,如有錯誤,輕噴

下集預告

【小小前端】前端排序演算法第二期(繞人的希爾排序)

相關文章