最簡單的冒泡排序還能怎麼優化?

摘要: 冒泡排序應該是大部分人學到的第一個排序算法, 它思想簡單, 是入門排序算法的好選擇. 然而因爲它的時間複雜度爲O(n^2), 因此除了學習它的時間以外咱們就不多的想到它了, 一般提到更多的仍是快速排序等時間複雜度更低的排序算法. 然而, 在對經典的冒泡排序進行改善以後, 在必定的條件之下, 仍然有它的用武之地.算法

本文首先介紹了 3 種對經典冒泡排序的改進思想, 而後將這 3 種思想結合起來, 實現綜合了各自優勢的方法.數組

冒泡排序的經典實現

再也不用不少篇幅來討論冒泡排序的思想, 簡而言之它是經過兩兩比較並交換而將最值放置到數組的最後位置. 具體實現能夠用雙層循環, 外層用來控制內層循環中最值上浮的位置, 內層用來進行兩兩比較和交換位置.函數

以將數組從小到大排序爲例, 下面的部分都默認如此. 冒泡排序的經典實現以下:學習

function bubbleSort(array){
    // 外層循環使用 end 來控制內層循環中極值最終上浮到的位置
    for(let end = array.length - 1; end > 0; end--){
        // 內層循環用來兩兩比較並交換
        for(let i = 0; i < end; i++){
            if(array[i] > array[i + 1]){
                swap(array, i, i+1);
            }
        }
    }
}
複製代碼

上面代碼中用到函數 swap() 來交換數組兩個位置的元素, 在下面的代碼中都會用到這個函數, 具體以下:ui

function swap(arr, i, j){
    // [arr[i],arr[i+1]] = [arr[i+1],arr[i]]; // ES6
    
    const temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}
複製代碼

改進 一: 處理在排序過程當中數組總體已經有序的狀況

若數組原本就是有序的或者在排序的過程當中已經有序, 則沒有必要繼續下面的比較, 能夠直接返回這個數組. 可是冒泡排序的經典實現仍然會繼續挨個訪問每一個元素而且比較大小. 雖然這個時候只有比較操做而沒有交換操做, 但這些比較操做仍然是沒有必要的.this

數組已經有序的標誌是在一趟內層循環中沒有發生元素的位置交換(swap)操做, 也就是說從開頭到結尾的每一個元素都小於它以後的元素.spa

利用上面的原理, 能夠對經典實現進行改進: 設置一個變量用來記錄在一輪內層循環中是否發生過元素的交換操做, 並在每一輪內層循環結束後判斷是否發生了元素交換. 若沒有發生元素交換, 則說明數組已有序, 程序返回; 不然不作任何操做, 開始下一輪循環:code

function bubbleSortOpt1(array){
    
    for(let end = array.length - 1; end > 0; end--){

        let isSorted = true; // <== 設置標誌變量 isSorted 初始值爲 true
        for(let i = 0; i < end; i++){
            if(array[i] > array[i + 1]){
                swap(array, i, i+1);

                isSorted = false;  // <== 發生了交換操做, 說明再這一輪中數組仍然無序, 將變量 isSorted 設置爲 false
            }
        }

        // 在一輪內層循環後判斷 是否有序, 如有序則直接 中止程序; 不然開始下一輪循環
        if(isSorted){  
            return ;  // <== 數組已經有序, 中止函數的執行
        }
    }
}
複製代碼

改進思想 二: 數組局部有序

若數組是局部有序的, 例如從某個位置開始以後的數組已經有序, 則沒有必要對這一部分數組進行比較了.排序

此時的改進方法是: 在遍歷過程當中能夠記下最後一次發生交換事件的位置, 下次的內層循環就到這個位置終止, 能夠節約多餘的比較操做.事件

使用一個變量來保存最後一個發生了交換操做的位置, 並設置爲下一輪內層循環的終止位置:

function bubbleSortOpt2(array){
    let endPos = array.length - 1; // 記錄這一輪循環最後一次發生交換操做的位置

    while(endPos > 0){
        let thisTurnEndPos = endPos; // <== 設置這一輪循環結束的位置

        for(let i = 0; i < thisTurnEndPos; i++){
            if(array[i] > array[i+1]){
                swap(array, i, i+1);

                endPos = i; // <== 設置(更新)最後一次發生了交換操做的位置
            }
        }
        
        // 若這一輪沒有發生交換,則證實數組已經有序,直接返回便可
        if(endPos === thisTurnEndPos){ 
	        return ;
	    }
    }
}
複製代碼

改進思想 三: 同時將最大最小值歸位

在經典實現中, 每次將最大的值調整到當前數組的最後, 而沒有對最小的值進行操做. 其實在同一輪外層循環中, 能夠在把最大值調整到數組最後面的同時和把最小值調整到最前面, 只要在內層循環中在從前到後安排最大值的同時, 也從後向前安排這些最小值的位置就能夠了, 這種思想稱爲雙向冒泡排序.

提及來比較抽象, 看代碼就比較容易明白了:

// 雙向冒泡排序, 不只把最大的放到最後, 同時把最小的放到最前
function bubbleSortOpt3(array){
    // <== 設置每一輪循環的開始與結束位置
    let start = 0, 
        end = array.length - 1;

    while(start < end){
        for(let i = start; i < end; i++){ // 從start位置end位置過一遍安排最大值的位置
            if(array[i] > array[i+1]){
                swap(array, i, i+1);
            }
        }
        end --; // <== 因爲當前最大的數已經放到了 end 位置, 故 end 位置向前移動

        for(let i = end; i > start; i--){ // 從end向start位置過一遍, 安排最小值的位置
            if(array[i] < array[i-1]){
                swap(array, i, i-1);
            }
        }
        start ++; // <== 因爲當前最小的數已經放到了 start 位置, 故 start 位置向後移動
    } 
}
複製代碼

然而這種方法也有個缺點, 即每次向前向後移動一個位置, 即end--start++. 沒法處理前面部分所說的兩種狀況, 因此能夠將這三種方法結合起來發揮各自的優點.

三種思想的結合

以上三種思想分別處理在排序過程當中數組總體已經有序、數組局部有序、同時將最大最小值放置在合適位置的狀況. 那麼將以上三者的優勢結合起來能夠達到更好的效果.

按部就班, 先說說其中兩種思想的結合

思想一、2的結合

將思想1和2結合起來, 處理數組局部有序和排序過程當中總體有序的狀況:

function bubbleSortOpt1and2(array){
    let endPos = array.length - 1; // 記錄下一輪循環結束的位置, 也就是上一輪最後交換的位置

    while(endPos > 0){
        let isSorted = true; // 設置數組總體有序標誌變量
        let thisTurnEndPos = endPos; // 記錄這一輪循環結束的位置

        for(let i = 0; i < thisTurnEndPos; i++){
            if(array[i] > array[i+1]){
                swap(array, i, i+1);

                endPos = i; // 這個位置發生了交換, 將這個位置記錄下來
                isSorted = false;  // 設置本輪爲無序
            }
        }

        if(isSorted){ // 判斷數組是否已經總體有序
            console.log(endPos);
            return;
        }
    }
}
複製代碼

思想二、3的結合

將思想 2和3 結合起來, 從雙向同時處理最大最小值, 並且處理數組局部有序的狀況

// 結合第二、3種改進方式的思想, 記錄雙向排序中每一個方向的最後交換位置, 並更新下一輪循環的結束位置
function bubbleSortOpt2and3(array){
    let start = 0, startPos = start,
        end = array.length - 1, endPos = end;

    while(start < end){
        
        // 從前向後過一遍
        for(let i = start; i < end; i++){ 
            if(array[i] > array[i+1]){
                swap(array, i, i+1);
                endPos = i; // 記錄這個交換位置
            }
        }
        end = endPos;  // 設置下一輪的遍歷終點

        // 從後向前過一遍
        for(let i = end; i > start; i--){ 
            if(array[i] < array[i-1]){
                swap(array, i, i-1);
                startPos = i; // 記錄這個交換位置
            }
        }
        start = startPos; // 設置下一輪的遍歷終點

    }
}
複製代碼

同時使用以上三種思想

在有了兩兩結合的基礎後, 不難寫出將這三種思想結合的代碼:

function bubbleSortOptTriple(array){
    let start = 0, startPos = start,
        end = array.length - 1, endPos = end;

    while(start < end){
        let isSorted = true; // 設置有序無序的標誌變量
        // 從前向後過一遍
        for(let i = start; i < end; i++){ 
            if(array[i] > array[i+1]){
                swap(array, i, i+1);

                endPos = i; // 記錄這個交換位置
                isSorted = false; // 設置無序標誌
            }
        }

        if(isSorted){
            return;
        }

        end = endPos;  // 設置下一輪的遍歷終點
        

        // 從後向前過一遍
        for(let i = end; i > start; i--){ 
            if(array[i] < array[i-1]){
                swap(array, i, i-1);

                startPos = i; // 記錄這個交換位置
                isSorted = false; // 設置無序標誌
            }
        }

        if(isSorted){
            return;
        }

        start = startPos; // 設置下一輪的遍歷終點
    }
}
複製代碼

這就是終點了嗎?

其實上面的程序仍是能夠改進的: 咱們沒有必要另外設置一個變量來記錄在一趟排序中數組是否已經有序,而是能夠比較一輪循環結束後的 endPos 是否等於end, 若是等於, 則說明本輪沒有對 endPos 進行更新, 也就是沒有發生交換操做, 進一步說明數組已經有序了. 固然, 對於startPosstart同理.

相關文章
相關標籤/搜索