常用的比較排序演算法總結

糊糊糊糊糊了發表於2018-04-11

寫在前面

一直很懼怕演算法,總是感覺特別傷腦子,因此至今為止,幾種基本的排序演算法一直都不是很清楚,更別說時間複雜度、空間複雜度什麼的了。

今天抽空理了一下,其實感覺還好,並沒有那麼可怕,雖然程式碼寫出來還是磕磕絆絆,但是思想和原理還是大致上摸清楚了,記錄、分享。

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

說明

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

排序演算法分類

  • 比較排序,時間複雜度為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);

過程大致如下:

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

氣泡排序

氣泡排序每次從陣列的最開始索引處與後一個值進行比較,如果當前值比較大,則交換位置。這樣一次迴圈下來,最大的值就會排入到最後的位置。

// 分類 -------------- 內部比較排序
// 資料結構 ---------- 陣列
// 最差時間複雜度 ---- 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

插入排序

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

// 分類 ------------- 內部比較排序
// 資料結構 ---------- 陣列
// 最差時間複雜度 ---- 最壞情況為輸入序列是降序排列的,此時時間複雜度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

更多請參看https://www.cnblogs.com/skywang12345/p/3602162.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

參考

相關文章