經常使用的比較排序算法總結

寫在前面

一直很害怕算法,老是感受特別傷腦子,所以至今爲止,幾種基本的排序算法一直都不是很清楚,更別說時間複雜度、空間複雜度什麼的了。javascript

今天抽空理了一下,其實感受還好,並無那麼可怕,雖然代碼寫出來仍是磕磕絆絆,可是思想和原理仍是大體上摸清楚了,記錄、分享。html

另外一篇文章:三種非比較排序算法總結java

說明

關於排序,前輩們已經講解的夠多了,我這裏主要摘錄一些概念。算法

排序算法分類

  • 比較排序,時間複雜度爲O(nlogn) ~ O(n^2),主要有:冒泡排序,選擇排序,插入排序,歸併排序,堆排序,快速排序
  • 非比較排序,時間複雜度能夠達到O(n),主要有:計數排序,基數排序,桶排序

排序穩定性

排序算法穩定性的簡單形式化定義爲:若是Ai = Aj,排序前Ai在Aj以前,排序後Ai還在Aj以前,則稱這種排序算法是穩定的。數組

選擇排序

選擇排序每次比較的是數組中特定索引的值與全數組中每一個值的大小比較,每次都選出一個最小(最大)值,若是當前索引的值大於以後索引的值,則二者進行交換數據結構

// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- O(n^2)
// 最優時間複雜度 ---- O(n^2)
// 平均時間複雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 不穩定

var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6];
var temp;

for (var i = 0; i < arr.length; i++) {
    for (var j = i + 1; j < arr.length; j++) {
        if (arr[i] > arr[j]) {
            temp = arr[j];
            arr[j] = arr[i];
            arr[i] = temp;
        }
    }
}

console.log(arr);

過程大體以下:ui

1 4 5 2 3 9 0 7 6
0 4 5 2 3 9 1 7 6
0 2 5 4 3 9 1 7 6
0 1 5 4 3 9 2 7 6
0 1 4 5 3 9 2 7 6
0 1 3 5 4 9 2 7 6
0 1 2 5 4 9 3 7 6
0 1 2 4 5 9 3 7 6
0 1 2 3 5 9 4 7 6
0 1 2 3 4 9 5 7 6
0 1 2 3 4 5 9 7 6
0 1 2 3 4 5 7 9 6
0 1 2 3 4 5 6 9 7
0 1 2 3 4 5 6 7 9

冒泡排序

冒泡排序每次從數組的最開始索引處與後一個值進行比較,若是當前值比較大,則交換位置。這樣一次循環下來,最大的值就會排入到最後的位置。.net

// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- O(n^2)
// 最優時間複雜度 ---- 若是能在內部循環第一次運行時,使用一個旗標來表示有無須要交換的可能,能夠把最優時間複雜度下降到O(n)
// 平均時間複雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 穩定

var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6];
var t;

for (var m = 0; m < arr.length; m++) {
    for (var n = 0; n < arr.length - m; n++) {
        if (arr[n] > arr[n + 1]) {
            t = arr[n + 1];
            arr[n + 1] = arr[n];
            arr[n] = t;
        }
    }
}

console.log(arr);

過程大體以下:設計

1 4 5 2 3 9 0 7 6
1 4 2 5 3 9 0 7 6
1 4 2 3 5 9 0 7 6
1 4 2 3 5 0 9 7 6
1 4 2 3 5 0 7 9 6
1 4 2 3 5 0 7 6 9
1 2 4 3 5 0 7 6 9
1 2 3 4 5 0 7 6 9
1 2 3 4 0 5 7 6 9
1 2 3 4 0 5 6 7 9
1 2 3 0 4 5 6 7 9
1 2 0 3 4 5 6 7 9
1 0 2 3 4 5 6 7 9
0 1 2 3 4 5 6 7 9

插入排序

插入排序相似於撲克牌的插入方法,選取待排列數組中的任意一個數字做爲已排序的基準,再依次從待排序數組中取出數字,根據依次比較,將這個數字插入到已排序的數組中code

// 分類 ------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- 最壞狀況爲輸入序列是降序排列的,此時時間複雜度O(n^2)
// 最優時間複雜度 ---- 最好狀況爲輸入序列是升序排列的,此時時間複雜度O(n)
// 平均時間複雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 穩定

var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6];

/**
 * 直接使用同一個數組方式
 */
for (var i = 1; i < arr.length; i++) {
    var get = arr[i];
    var j = i - 1;
    // 倒敘比較已經排序的值和取到的值進行比較
    // 若是取到的值在已經排序中的值中存在合適的索引插入,則須要將這個索引以後的值進行後移
    while (j >= 0 && arr[j] > get) {
        arr[j + 1] = arr[j];
        j--;
    }
    arr[j + 1] = get;
}
console.log(arr);

/**
 * 引入一個新的數組方式
 * 引入一個數組後會更好理解
 */
var sortList = [arr[0]];

for (var i = 1; i < arr.length; i++) {
    var sLen = sortList.length;

    // 若是取出的數字比已經排序的第一個值都小,則插入到最開始
    if (arr[i] < sortList[0]) {
        sortList.unshift(arr[i])
        continue;
    }

    // 若是取出的數字比已經排序的最後一個值都大,則插入到最末尾
    if (arr[i] > sortList[sLen - 1]) {
        sortList[sLen] = arr[i];
        continue;
    }

    for (var j = 0; j < sLen - 1; j++) {
        if (arr[i] >= sortList[j] && arr[i] <= sortList[j + 1]) {
            sortList.splice(j + 1, 0, arr[i]);
            break;
        }       
    }
}

console.log(sortList);

過程大體以下:

1
1 4
1 4 5
1 2 4 5
1 2 3 4 5
1 2 3 4 5 9
0 1 2 3 4 5 9
0 1 2 3 4 5 7 9
0 1 2 3 4 5 6 7 9

二分插入排序

二分插入排序是直接插入排序的一個變種,利用二分查找法找出下一個插入數字對應的索引,而後進行插入。

當n較大時,二分插入排序的比較次數比直接插入排序的最差狀況好得多,但比直接插入排序的最好狀況要差,所當以元素初始序列已經接近升序時,直接插入排序比二分插入排序比較次數少。二分插入排序元素移動次數與直接插入排序相同,依賴於元素初始序列。

// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- O(n^2)
// 最優時間複雜度 ---- O(nlogn)
// 平均時間複雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 穩定

var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6];

/**
 * 直接使用同一個數組方式
 */
for (var i = 1; i < arr.length; i++) {
    var get = arr[i];
    var left = 0;
    var right = i - 1;

    // 每次找出中間位置而後進行比較,最終肯定索引位置
    while (left <= right) {
        var mid = parseInt((left + right) / 2);
        if (arr[mid] > get) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    
    for (var k = i - 1; k >= left; k--) {
        arr[k + 1] = arr[k];
    }
    arr[left] = get;
    
}

/**
 * 引入一個新的數組方式
 * 引入一個數組後會更好理解變化的方式
 */
var sortList = [arr[0]];

for (var i = 1; i < arr.length; i++) {
    var sLen = sortList.length;
    var get = arr[i];
    var left = 0;
    var right = sLen - 1;

    // 每次找出中間位置而後進行比較,最終肯定索引位置
    while (left <= right) {
        var mid = parseInt((left + right) / 2);
        if (sortList[mid] > get) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }

    // splice是數組插入值的一個快捷方式,將值移位的方式以下
    // sortList.splice(left, 0, get);
    
    for (var k = sLen - 1; k >= left; k--) {
        sortList[k + 1] = sortList[k];
    }
    sortList[left] = get;
    
}

console.log(sortList);

過程大體以下:

1
1 4
1 4 5
1 2 4 5
1 2 3 4 5
1 2 3 4 5 9
0 1 2 3 4 5 9
0 1 2 3 4 5 7 9
0 1 2 3 4 5 6 7 9

希爾排序

希爾排序是一種更高效的插入排序,經過設計步長(gap)將數組分組,而後每組中單獨採用排序算法將每組排序,而後在縮小步長,進行重複的分組排序工做,直到gap變爲1的時候,整個數組分爲一組,算法結束。

例如:數組 [1, 4, 5, 2, 3, 9, 0, 7, 6],若是每次以數組長度的一半來做爲步長,能夠分解爲如下步驟

1. gap: Math.floor(9 / 2) = 4;

分爲四組,分組爲: 
{ 1, 3 }, { 4, 9 }, { 5, 0 }, { 2, 7 }

最後一個數字 6 須要等到第5個數字排序完成,也就是3,能夠得出3依舊還處在第4索引的位置,所以最後一個分組爲 { 3, 6 }

完成一輪分組以及排序後的數組爲:[ 1, 4, 0, 2, 3, 9, 5, 7, 6 ]

2. gap: Math.floor(4 / 2) = 2;

分爲兩組,分組爲:
{ 1, 0, 3, 5, 6 }, { 4, 2, 9, 7 }

完成第二輪分組以及排序後的數組爲:[ 0, 2, 1, 4, 3, 7, 5, 9, 6 ]

3. gap: Math.floor(2 / 2) = 1;

分爲一組,即爲:{ 0, 2, 1, 4, 3, 7, 5, 9, 6 }

完成第三輪分組以及排序後的數組爲:[ 0, 1, 2, 3, 4, 5, 6, 7, 9 ]
// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- 根據步長序列的不一樣而不一樣。已知最好的爲O(n(logn)^2)
// 最優時間複雜度 ---- O(n)
// 平均時間複雜度 ---- 根據步長序列的不一樣而不一樣。
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 不穩定

var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6];
var gap = Math.floor(arr.length / 2);

function swap(arr, i, j) {
    var t;
    t = arr[j];
    arr[j] = arr[i];
    arr[i] = t;
}

for (; gap > 0; gap = Math.floor(gap / 2)) {
    //從第gap個元素,逐個對其所在組進行直接插入排序操做
    for(var i = gap; i < arr.length; i++) {
        var j = i;
        // 這裏採用的實際上是冒泡排序
        while(j - gap >= 0 && arr[j] < arr[j-gap]) {
            //插入排序採用交換法
            swap(arr, j, j-gap);
            j -= gap;
        }
        
        // 或者插入排序
        var temp = arr[j];
        if (arr[j] < arr[j-gap]) {
            while (j-gap >= 0 && temp < arr[j-gap]) {
                arr[j] = arr[j-gap];
                j -= gap;
            }
            arr[j] = temp;
        }
    }
}

console.log(arr);

過程大體以下:

1 4 5 2 3 9 0 7 6
1 4 0 2 3 9 5 7 6
0 4 1 2 3 9 5 7 6
0 2 1 4 3 9 5 7 6
0 2 1 4 3 7 5 9 6
0 1 2 4 3 7 5 9 6
0 1 2 3 4 7 5 9 6
0 1 2 3 4 5 7 9 6
0 1 2 3 4 5 7 6 9
0 1 2 3 4 5 6 7 9

歸併排序

歸併排序採用的是一種分治思想,將整個數組遞分紅若干小組,直到最後組中的個數爲1時中止,那麼此時再與同一級別的分組數字進行比較,這就是的操做。而後向上一層層地進行合併,最終合成一個排序好的數組。

這麼講可能有點糊塗,用一個例子分析。好比如今有這兩個排序好的數組

var a = [1, 4, 6, 7, 9];
var b = [2, 3, 5, 8];
var temp = [];

// 比較過程以下:
// 比較兩個數組中的第一個數字,將數字小的壓進temp數組,同時將這個數字從原數組中刪除

// 第一步
a[0] < b[0] 
// 獲得
a: [4, 6, 7, 9]
b: [2, 3, 5, 8]
temp: [1]

// 第二步
a[0] > b[0]
// 獲得
a: [4, 6, 7, 9]
b: [3, 5, 8]
temp: [1, 2]

// 第三步
a[0] > b[0]
// 獲得
a: [4, 6, 7, 9]
b: [5, 8]
temp: [1, 2, 3]

// 中間省略N步

// 第N+1步
a: [9]
b: []
temp: [1, 2, 3, 4, 5, 6, 7, 8]
// 此時b數組已經爲空,則直接歸併
// 獲得
a: []
b: []
temp: [1, 2, 3, 4, 5, 6, 7, 8, 9]

注:以上的步驟只是歸併排序遞歸中的最上層的一步,其中下面還會分紅不少小的合併步驟。

// 分類 -------------- 內部比較排序
// 數據結構 ---------- 數組
// 最差時間複雜度 ---- O(nlogn)
// 最優時間複雜度 ---- O(nlogn)
// 平均時間複雜度 ---- O(nlogn)
// 所需輔助空間 ------ O(n)
// 穩定性 ------------ 穩定

var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6];
var len = arr.length;

function mergeArray(arr, first, mid, last, t) {
    var i = mid, 
        j = last,
        m = first,
        n = mid + 1,
        k = 0;

    while (m <= mid && n <= last) {
        if (arr[m] > arr[n]) {
            t[k++] = arr[n++];
        } else {
            t[k++] = arr[m++];
        }
    }

    while (m <= i) {
        t[k++] = arr[m++]
    }

    while(n <= j) {
        t[k++] = arr[n++];
    }

    for (var p = 0; p < k; p++) {
        arr[first + p] = t[p];
    }
}

function mergeSort(arr, first, last, t) {
    if (first < last) {
        var mid = Math.floor((first + last) / 2);
        mergeSort(arr, first, mid, t);
        mergeSort(arr, mid + 1, last, t)
        mergeArray(arr, first, mid, last, t);
    }
}

mergeSort(arr, 0, len - 1, []);

console.log(arr);

過程大體以下:

1 4 5 2 3 9 0 7 6
1 4 5 2 3 9 0 7 6
1 4 5 2 3 9 0 7 6
1 4 5 2 3 9 0 7 6
1 2 3 4 5 9 0 7 6
1 2 3 4 5 0 9 7 6
1 2 3 4 5 0 9 6 7
1 2 3 4 5 0 6 7 9
0 1 2 3 4 5 6 7 9

快速排序

快速排序的原理是:首先隨機選擇一個值,遍歷整個數組,比這個值小的放在左邊的數組中,比這個值大的放在右邊的數組中,而後再根據上一步得出的左右數組重複上述的操做,直到分出的左右數組長度爲1或者0的時候中止。

仍是舉個栗子吧:

var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6];

// 1. 選取一個數,我這裏取中間的數,即爲arr[4] = 3
left: [1, 2, 0]
right: [4, 5, 9, 7, 6]

// 2. 在左右數組中重複上述操做
left: [1, 2, 0]
取數:left[1] = 2

left-left: [0, 1]   // 繼續遞歸
left-right: []      // 遞歸結束,直接返回

right: [4, 5, 9, 7, 6]
取數: right[3] = 9
right-left: [4, 5, 7, 6]    // 繼續遞歸
right-right: []             // 遞歸結束,直接返回

在遞歸中排序,而後鏈接選出的那個數,就完成了整個數組的排序

var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6];

function quickSort(arr) {
    if (arr.length === 1 || arr.length === 0) {
        return arr;
    }

    var left = [];
    var right = [];
    var len = arr.length;
    var f = 0;
    var l = len - 1;
    var mid = Math.floor((f + l) / 2);
    var midVal = arr[mid];

    for (var i = 0; i < len; i++) {
        if (arr[i] < arr[mid]) {
            left.push(arr[i]);
        } else if (arr[i] > arr[mid]) {
            right.push(arr[i])
        }
    }

    var leftArr = quickSort(left);
    var rightArr = quickSort(right);

    return leftArr.concat(midVal).concat(rightArr);
}

var result = quickSort(arr);
console.log(result);

大體過程以下:

left:    1 2 0
middle:  3
right:   4 5 9 7 6

left:    1 0
middle:  2
right:  

left:    0
middle:  1
right:  

left:    4 5 7 6
middle:  9
right:  

left:    4
middle:  5
right:   7 6

left:    6
middle:  7
right:

堆排序

堆排序是指利用堆這種數據結構所設計的一種選擇排序算法。堆是一種近似徹底二叉樹的結構(一般堆是經過一維數組來實現的),並知足性質:以最大堆(也叫大根堆、大頂堆)爲例,其中父結點的值老是大於它的孩子節點。

咱們能夠很容易的定義堆排序的過程:

  • 由輸入的無序數組構造一個最大堆,做爲初始的無序區
  • 把堆頂元素(最大值)和堆尾元素互換
  • 把堆(無序區)的尺寸縮小1,並調用heapAdjust(arr, 0)重新的堆頂元素開始進行堆調整
  • 重複步驟2,直到堆的尺寸爲1

更多請參看http://www.javashuo.com/article/p-hjboauhu-dg.html,這篇文章中進行了很詳細地講解。

var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6];
var len = arr.length;

function swap(arr, i, j) {
    var t = arr[j];
    arr[j] = arr[i];
    arr[i] = t;
}

function heapAdjust(arr, i, end) {
    var left = 2 * i + 1;               // 左邊子節點
    var right = 2 * i + 2;              // 右側子節點
    var max = i;

    if (left < end && arr[left] > arr[max]) {
        max = left;
    }

    if (right < end && arr[right] > arr[max]) {
        max = right;
    }

    if (max !== i) {
        swap(arr, max, i);
        heapAdjust(arr, max, end);
    }
}

function buildMaxHeap(arr, len) {
    var sNode = Math.floor(len / 2) - 1;    // 第一個須要調整的非葉子節點
    for (var i = sNode; i >= 0; i--) {
        heapAdjust(arr, i, len);
    }
    return len;
}

function heapSort(arr) {
    var heapSize = buildMaxHeap(arr, len);

    // 堆(無序區)元素個數大於1,未完成排序
    while (heapSize > 1) {
        // 將堆頂元素與堆的最後一個元素互換,並從堆中去掉最後一個元素
        // 此處交換操做頗有可能把後面元素的穩定性打亂,因此堆排序是不穩定的排序算法
        swap(arr, 0, --heapSize);
        // 重新的堆頂元素開始向下進行堆調整,時間複雜度O(logn)
        heapAdjust(arr, 0, heapSize);     
    }
}

heapSort(arr);
console.log(arr);

大體實現以下:

1 4 5 2 3 9 0 7 6
7 6 5 4 3 1 0 2 9
6 4 5 2 3 1 0 7 9
5 4 1 2 3 0 6 7 9
4 3 1 2 0 5 6 7 9
3 2 1 0 4 5 6 7 9
2 0 1 3 4 5 6 7 9
1 0 2 3 4 5 6 7 9
0 1 2 3 4 5 6 7 9

參考

相關文章
相關標籤/搜索