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

算法分類

十種常見排序算法能夠分爲兩大類:javascript

非線性時間比較類排序:經過比較來決定元素間的相對次序,因爲其時間複雜度不能突破O(nlogn),所以稱爲非線性時間比較類排序。html

線性時間非比較類排序:不經過比較來決定元素間的相對次序,它能夠突破基於比較排序的時間下界,以線性時間運行,所以稱爲線性時間非比較類排序。前端

算法複雜度

相關概念

穩定:若是a本來在b前面,而a=b,排序以後a仍然在b的前面。java

不穩定:若是a本來在b的前面,而a=b,排序以後 a 可能會出如今 b 的後面。算法

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

空間複雜度:是指算法在計算機內執行時所需存儲空間的度量,它也是數據規模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和46ui

從代碼中看出,排序過程當中,所須要的臨時變量一直都沒有變化,所以空間複雜度爲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²)。

總結

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

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

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

使用場景

對於通常工做來講,這三種使用沒什麼體驗上的差距

若是非要選擇的話,插入排序比選擇排序少一些比較的次數,但選擇排序有時候比插入排序少挪動次數,建議數據較大時用插入排序,數據量較小時能夠用選擇排序。(其實這倆差很少--)

參考

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

下集預告

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

相關文章
相關標籤/搜索