淺談排序算法

本文引至: 排序算法算法

計算機中兩大主要的算法,一個是排序,一個是索引. 基於這兩個基本的算法, 咱們應該瞭解一下最基本的內容。 這裏, 咱們先介紹一下排序算法.shell

冒泡排序

冒泡排序(bubble sort) 是一種比較簡單的排序方法, 但他的速度也是最慢的一種. 他是經過循環比較序列, 而後將大的移到後面, 小的放到前面. 更形象的理解, 能夠參考 bubble sort 動態演示.
這裏, 咱們經過對數組的比較來實現一個簡單的冒泡排序.數組

/**
 * 生成隨機數組
 */
function randomArr() {
    let arr = [];
    for (var i = 0; i < 50; i++) {
        arr.push(i);
    }
    arr.sort(() => Math.random() - 0.5);
    return arr;
}
let arr = randomArr();

/**
 * 冒泡排序
 * @param  {Array} arr 亂序數組
 */
function bubbleSort(arr){
    for(var j=arr.length;j>=2;j--){
        for(var i=0;i<j;i++){
            if(arr[i]>arr[i+1]){
            // 交換數組值
                [arr[i],arr[i+1]] = [arr[i+1],arr[i]];
            }
        }
    }
    return arr;
}

arr = bubbleSort(arr);
console.log(arr);

咱們能夠看一下他的複雜度:dom

type complexity
最壞狀況的排序 O(n^2)
最好狀況的排序 O(n)
平均性能 O(n^2)
空間複雜度 O(1)

實際上, 這只是很基礎的一部分. 若是想須要計算機, 真的須要不少實戰場景才行.性能

選擇排序

他原理是, 從列表的第一個位置開始, 與剩餘位置進行比較, 若是存在比第一個位置小的,那麼和他進行交換. 這樣,最小的元素就到第一個位置上去了. 而後從第二個位置開始, 又和剩餘的元素比較, 接着第二小的元素,就到第二個位置上. 以此類推, 知道全部元素排序列徹底. 動態內容請參考: selection sort測試

接着咱們使用代碼來模擬一下:大數據

let arr = randomArr();

/**
 * 選擇排序
 * @param  {Array} arr 亂序排序的數組
 * @return {Array}     返回新的排列數組
 */
function selectSort(arr){
    for(var i=0;i<arr.length;i++){
        for(var j=arr.length-1;j>i;j--){
            if(arr[i]>arr[j]){
                [arr[i],arr[j]] = [arr[j],arr[i]];
            }
        }
    }
    return arr;
}

arr = selectSort(arr);
console.log(arr);

他的複雜度,很好理解: 時間上是O(n^2) 且不論任何狀況. 空間上是O(1) 就至關於基本不會變的那種.ui

插入排序

插入排序的原理是: 從第二個位置開始, 與前一個元素進行比較,若是該元素比較小, 則將第一個元素移到第二個元素上, 而後將該元素插入到第一個元素。 而後到第三個位置(3ele), 將3ele與前兩個元素進行比較, 若是第二個元素比其大, 則日後移動移動一位,直到找到比3ele小的位置並插入. 最好仍是看動態圖:insert
咱們使用算法來模擬一下:spa

/**
 * 插入排序
 * @param  {Array} arr 亂序排序的數組
 * @return {Array}     返回新的排列數組
 */
function inertSort(arr){
    for(var i=1,len=arr.length;i<len;i++){
        let target = arr[i];
        for(var j=i;j>=0;j--){
            if(target<arr[j-1]){
                // 比目標節點大,則將位置後移, 繼續下一次比較
                arr[j] = arr[j-1];
            }else{
                // 若是比目標節點小, 則結束比較, 並插入到當前節點位置
                arr[j] = target;
                break;
            }
        }
    }
    return arr;
}

arr = inertSort(arr);
console.log(arr);

看一下動圖: from wiki
insert sort
一樣, 該算法的時間複雜度和算則排序相似爲O(n^2),空間複雜度爲O(1).net

type complexity
最壞狀況的排序 O(n^2)
最好狀況的排序 O(n)
平均性能 O(n^2)
空間複雜度 O(1)

這裏就是最基本的排序算法. 作一下總結:

simple sort

3中基本的算法,咱們差很少了解了. 但他們之間孰優孰劣,咱們還不太清楚, 咱們能夠寫一個簡單的測試類,來看看, 他們之間的速度.
詳細代碼,我放在JSfiddler中了, 有興趣的能夠看一看,我這裏只把結果列一下:
基本結果以下:

bubbleSort time is
1: 39.820ms
selectSort time is
2: 18.385ms
insertSort time is
3: 3.907ms

這是創建在1000個元素大小的arr上. 可見, size越大, 他們以前性能差異越大. 基本的速度是:

insert > select > bubble

主要是由於

  • insert 是隻要找到比其target小的就結束一輪循環. 而且,他的比較次數, 是由小到大的, 而且很大概率是小於最大長度.

  • 而select的比較次數是從數組長度開始的(最大長度), 而且每一輪都會所有比較, 這就有點尷尬了.

  • bubble 我就不囉嗦了. 太累人了

因此, 最後咱們能夠總結一下:

sort

高級排序算法

所謂的高級其實是針對於大數據來講的. 上面簡單的排序正對於10^4 量級的已經夠了。 可是若是你想提升的話, 則可能就要上一個level了.

希爾排序

希爾是我的名, 咱們也能夠叫作ShellSort. 他其實是基於插入排序的, 因爲插入排序是3中基本排序中最快的, 因此, 若是想要提高效率和速度的話, 最快捷的辦法就是基於他了.
插入排序 默認的比較間隔是1,若是他的目標值,恰好在比較範圍的另一端的話, 那麼基本上,這就比較心累了. 因此, 爲了解決在大數據中遇到的問題, Shell 提出了一種排序, 即, 指定序列間隔進行相關排序. 原來的插入排序比較間隔是1, 那麼這裏就能夠改成[5,3,1],若是size更大, 序列間隔大小還能夠改成[10,5,3,1]. Marcin Ciura 寫過一篇論文論證過這個步長值, 通常取701, 301, 132, 57, 23, 10, 4, 1. 這幾個, 希爾排序的性能會獲得最大的提高.
具體的動圖爲:

shell sort

不過, 估計也沒幾我的看懂, 這個仍是得多動手,纔會有點感受. 若是實在不懂, 能夠參考插入排序.
這裏,直接上代碼了:

/**
 * shell排序
 * @param  {Array} arr  亂序數組
 * @param  {Array} gaps 步長數組
 * @return {Array}      返回排序後的數組
 */
function shellSort(arr, gaps) {
    for (var gap of gaps) {
        // gap爲每一次的步長值
        // 將每次遍歷的第一個值(i) 設爲步長值
        for (var i = gap, len = arr.length; i < len; i++) {
            // 插入排序標準flag, 存儲比較值
            let target = arr[i];
            // 在for循環中比較, 若是比較值較大, 則將比較值右移動
            for (var j = i; j>=gap && arr[j-gap] > target;j-=gap){
                arr[j] = arr[j-gap]
            }
            // 比較完成後, 將target插入到適當位置
            arr[j]=target;
        }
    }
    return arr;
}

另外, 還有一種動態ShellSort. 是動態計算步長值. 他的原理是根據你的arr.length來肯定的.
咱們先看一下他生成步長序列的方法.(這是 Robert Sedgewick 寫的, 《算法》的合著者)

// 生成隨機數組
    const produceGaps = function() {
        var N = arr.length,
            h = 1,
            gaps = [];
        while (h < N / 3) {
            gaps.push(h);
            h = 3 * h + 1;
        }
        return gaps;
    }

最後, 實際的動態ShellSort爲

function DyShellSort(arr) {
    // 生成隨機數組
    const produceGaps = function() {
        var N = arr.length,
            h = 1,
            gaps = [];
        while (h < N / 3) {
            gaps.push(h);
            h = 3 * h + 1;
        }
        return gaps;
    }
    let gaps = produceGaps();

    function shellSort(arr, gaps) {
        for (var gap of gaps) {
            // gap爲每一次的步長值
            // 將每次遍歷的第一個值(i) 設爲步長值
            for (var i = gap, len = arr.length; i < len; i++) {
                // 插入排序標準flag, 存儲比較值
                let target = arr[i];
                // 在for循環中比較, 若是比較值較大, 則將比較值右移動
                for (var j = i; j >= gap && arr[j - gap] > target; j -= gap) {
                    arr[j] = arr[j - gap]
                }
                // 比較完成後, 將target插入到適當位置
                arr[j] = target;
            }
        }
        return arr;
    }
    return shellSort(arr,gaps);
}

實際上, 他們二者效率其實差很少, 動態的只是多出來一個生成隨機數組而已. 相比於 對超大量數組排序來講, 這點仍是不算什麼的.
因此, 上面的兩種方法, 你用哪一種都差很少.該算法的複雜度爲:

type complexity
最壞狀況的排序 O(O(nlog2 n)
最好狀況的排序 O(n)
平均時間複雜度 取決於間隔值的大小, 越大則越小
空間複雜度 O(1)

歸併排序(mergesort)

歸併排序,不一樣於上述全部的排序方法, 他是一種以犧牲空間換效率的算法. 歸併排序,有兩種排序方向,一種爲自頂向下(top-down),一種爲至底向上(down-top). 二者其實沒有什麼太大的卻別, 只是他們二者實現的方式是徹底相反的.

  • top-down: 現將原來的list 一步一步切分爲size爲1的sublist. 而且每一次拆分,都對將要分開的sublist, 做相關的排序, 最後,獲得size爲1的list, 合併這些list 就獲得sorted list. 但, 該方式可能會涉及遞歸, 比較困難, 對於js這種, 處理空間能力弱的, 該方式就不合適了.

  • down-top: 先將原來的list 直接切分爲size爲1的sublist, 而後逐步將兩兩sublist合併爲一個list, 一層一層, 最後合併爲一個完整有序的list.

具體,動圖爲; from wiki
mergesort

若是, 仍是不理解, 能夠參考動圖mergesort. 接下來, 咱們仍是照常的來實現一下歸併排序的算法.(至頂向下)。 實際算法如圖:

function mergerSort(arr) {
    let step = 1,
        len = arr.length;
    // 若是數組爲1|0則直接返回
    if (len < 2) return arr;
    let left_start, right_start;
    while (step < len) {
        left_start = 0;
        right_start = step;
        //當數組長度爲偶數時,而且小於數組總長度,執行循環
        while (right_start + step <= len) {
            // 分開對數組進行相關排序
            sortArr(arr,left_start,left_start+step,right_start,right_start+step);
            left_start = right_start+step;
            right_start  = left_start+step;
        }
        // 當數組長度爲奇數時, 再進行一次排序
        if(right_start < len){
            sortArr(arr,left_start,left_start+step,right_start,len);
        }
        step *= 2;
    }
    return arr;
}

/**
 * 對指定兩個子數組數組進行排序
 */
function sortArr(arr, left_start, left_end, right_start, right_end) {
    // 生成左右兩個數組
    let left_len = left_end - left_start,
        right_len = right_end - right_start,
        left_arr = new Array(left_len+1),
        right_arr = new Array(right_len+1);
    // 添加兩個數組的數據
    let k = left_start;
    for(var i=0;i<left_len;i++){
        left_arr[i] = arr[k+i];
    }
    k = right_start;
    for(var i=0;i<right_len;i++){
        right_arr[i] = arr[k+i];
    }
    // 放入一個無限大的數Infinity 方便下面比較
    left_arr[left_len] = right_arr[right_len] = Infinity;
    // 排序比較兩個數組
    // 已知,兩個數組都是左邊小右邊大.
    let left_index = 0,
    right_index = 0;

    for(var i=left_start;i<right_end;i++){
        if(left_arr[left_index]<=right_arr[right_index]){
            arr[i] = left_arr[left_index];
            left_index++;
        }else{
            arr[i] = right_arr[right_index];
            right_index++;
        }
    }
}

基本註釋已經寫的很清楚了, 具體demo代碼,放在JSfiddle中了.
他的基本複雜度爲:

type complexity
最壞狀況的排序 O(nlogn)
最好狀況的排序 O(n)
平均性能 O(nlogn)
空間複雜度 O(n)

快速排序(quick sort)

快排是一種比較浪的排序方法, 他的思想很簡單, 找到基準點, 分組. 一般狀況下, QS(quick sort)對於歸併排序和希爾排序能夠說是碾壓級的, 效率通常會高出1~2倍左右. why?
咱們說一下快排的原理, 你們大概就會清楚了

基本過程

  • 找到基準點(pivot), 一般狀況下以第一個, 固然, 也能夠說數組中任意一個. 只是第一個比較方便

  • 比較基準點. 小的放到基準點的左邊, 大的放在右邊

  • 遞歸上述步驟, 直到所有比較完畢.

從宏觀上來講, 快排其實能夠算3部分:

  • 基準值

  • 左邊數組

  • 右邊數組

這個就至關於咱們的二分法了. so, 快排又叫作二分比較法(partition-exchange sort)
摘自wiki的動圖:

QS sort

感受仍是挺好懂的. 接下來咱們使用代碼來模擬一下:

function QuickSort(arr){
    if(arr.length<2){
        return arr;
    }
    let pivot = arr[0],
        left_arr = [],
        right_arr = [];
    for(var i=1,len=arr.length;i<len;i++){
        if(arr[i]>pivot){
            right_arr.push(arr[i])
        }else{
            left_arr.push(arr[i]);
        }
    }
    return QuickSort(left_arr).concat([pivot],QuickSort(right_arr));
}

function randomArr() {
    let arr = [];
    for (var i = 0; i < 50; i++) {
        arr.push(i);
    }
    arr.sort(() => Math.random() - 0.5);
    return arr;
}
let arr = randomArr();

console.log(QuickSort(arr));

快排的基本複雜度爲:

type complexity
最壞狀況的排序 O(nlogn)
最好狀況的排序 O(n)
平均性能 O(nlogn)
空間複雜度 O(logn)

這裏,3種高級的排序已經說完了。 Shell Sort && Merge Sort 至關於只是瞭解一下, 快排纔是應該掌握而且要靈活運用的.

最後,咱們來總結一下吧:

advanced sort

相關文章
相關標籤/搜索