前端進階必備 — 手撕排序算法


 
  • 作者:陳大魚頭

  • github:https://github.com/KRISACHAN

算法是什麼?

算法(Algorithm) 已經是一個老生常談的概念了,最早來自於數學領域。

算法(Algorithm) 代表着用系統的方法描述解決問題的策略機制,可以通過一定規範的 輸入,在有限時間內獲得所需要的 輸出

如下圖示便是算法:

640?wx_fmt=png

算法的好壞

一個算法的好壞是通過 時間複雜度 與 空間複雜度 來衡量的。

舉個🌰:

640?wx_fmt=png

魚頭跟方勤一起去同一家公司面試,面試官讓他們實現同一個功能,巴拉巴拉大半天,兩個人終於交付了代碼。

面試官一運行,發現:

方勤的代碼運行一次要花 100ms ,佔用內存 5MB 。

而魚頭的代碼運行一次要花 100s ,佔用內存 500MB 。

好了,魚頭面試又失敗了!

640?wx_fmt=jpeg

以上所花的 時間 與 佔用內存 便是衡量一個 算法好壞的標準。

簡單來說,時間複雜度 就是執行算法的 時間成本 ,空間複雜度 則是執行算法的 空間成本 。

複雜度

時間複雜度 與 空間複雜度 都是用 “大O” 來表示,寫作 O(*)。有一點值得注意的是,我們談論複雜度,一般談論的都是時間複雜度。

常見時間複雜度的 “大O表示法” 描述有以下幾種:

時間複雜度 非正式術語
O(1) 常數階
O(n) 線性階
O(n2) 平方階
O(log n) 對數階
O(n log n) 線性對數階
O(n3) 立方階
O(2n) 指數階

一個算法在N規模下所消耗的時間消耗從大到小如下:

O(1) < O(log n) < O(n) < O(n log n) < O(n2) < O(n3) < O(2n)

上面括號的數據是啥意思?別問,問就讓你回去看數學書。

以下便爲不同時間複雜度的資源消耗增長圖示:

640?wx_fmt=png

常見概念:

  1. 最好時間複雜度: 在最理想情況下執行代碼的時間複雜度,它花的時間最短;

  2. 最壞時間複雜度: 最糟糕情況下執行代碼的時間複雜度,它花的時間最長;

  3. 平均時間複雜度: 執行代碼時間的平均水平,這個值就是概率論中的加權平均值,也叫期望值。

常見的排序算法

在生活中,我們離不開排序。例如體育課上按身高排的隊;又如考試過後按成績排的名次。

在編程中也是如此,例如當開發一個學生管理系統,需要按照學好從小到大進行排序;開發一個平臺,需要把同類商品按價格從高到低排序。(當然,一般前端不負責處理業務邏輯。)

有此可見,排序無處不在。

排序看似簡單,但是背後卻隱藏了多種多樣的算法與思想。

概述

根據時間複雜度的不同,常見的算法可以分爲3大類。

1.O(n²) 的排序算法

2.O(n log n) 的排序算法

3.線性的排序算法

各種排序的具體信息

640?wx_fmt=png

圖片解釋:

冒泡排序(Bubble Sort)

冒泡排序(Bubble Sort) 是一種基礎的 交換排序

冒泡排序之所以叫冒泡排序,是因爲它每一種元素都像小氣泡一樣根據自身大小一點一點往數組的一側移動。

算法步驟如下:

  1. 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個;

  2. 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數;

  3. 針對所有的元素重複以上的步驟,除了最後一個;

  4. 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。

圖示如下:

640?wx_fmt=gif

具體實現如下:

const bubbleSort = arr => {    const len = arr.length - 1    for (let i = 0; i < len; ++i) { /* 外循環爲排序趟數,len個數進行len-1趟 */        for (let j = 0; j < len - i; ++j) { /* 內循環爲每趟比較的次數,第i趟比較len-i次 */            if (arr[j] > arr[j + 1]) { /* 相鄰元素比較,若逆序則交換(升序爲左大於右,逆序反之) */                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]            }        }    }    return arr}arr => {
    const len = arr.length - 1
    for (let i = 0; i < len; ++i) { /* 外循環爲排序趟數,len個數進行len-1趟 */
        for (let j = 0; j < len - i; ++j) { /* 內循環爲每趟比較的次數,第i趟比較len-i次 */
            if (arr[j] > arr[j + 1]) { /* 相鄰元素比較,若逆序則交換(升序爲左大於右,逆序反之) */
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
            }
        }
    }
    return arr
}

選擇排序(Selection Sort)

選擇排序(Selection sort) 是一種簡單直觀的排序算法。

選擇排序的主要優點與數據移動有關。

如果某個元素位於正確的最終位置上,則它不會被移動。

選擇排序每次交換一對元素,它們當中至少有一個將被移到其最終位置上,因此對 n 個元素的表進行排序總共進行至多 n - 1 次交換。在所有的完全依靠交換去移動元素的排序方法中,選擇排序屬於非常好的一種。

選擇排序的算法步驟如下:

  1. 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;

  2. 然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾;

  3. 以此類推,直到所有元素均排序完畢。

圖示如下:

640?wx_fmt=gif

具體實現如下:

const selectionSort = arr => {    const len = arr.length    let min    for (let i = 0; i < len - 1; ++i) {        min = i /* 初始化未排序序列中最小數據數組下標 */        for (let j = i + 1; j < len; ++j) { /* 訪問未排序的元素 */            if (arr[j] < arr[min]) { /* 找到目前最小值 */                min = j /* 記錄最小值 */            }        }        [arr[i], arr[min]] = [arr[min], arr[i]] /* 交換位置 */    }    return arr}arr => {
    const len = arr.length
    let min
    for (let i = 0; i < len - 1; ++i) {
        min = i /* 初始化未排序序列中最小數據數組下標 */
        for (let j = i + 1; j < len; ++j) { /* 訪問未排序的元素 */
            if (arr[j] < arr[min]) { /* 找到目前最小值 */
                min = j /* 記錄最小值 */
            }
        }
        [arr[i], arr[min]] = [arr[min], arr[i]] /* 交換位置 */
    }
    return arr
}

插入排序(Insertion Sort)

插入排序(Insertion sort) 是一種簡單直觀的排序算法。

它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。

插入排序的算法步驟如下:

  1. 從第一個元素開始,該元素可以認爲已經被排序;

  2. 取出下一個元素,在已經排序的元素序列中從後向前掃描;

  3. 如果該元素(已排序)大於新元素,將該元素移到下一位置;

  4. 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置;

  5. 將新元素插入到該位置後;

  6. 重複步驟2~5。

圖示如下:

640?wx_fmt=gif

具體實現如下:


 
const insertionSort = arr => {    const len = arr.length    let j, temp    for (let i = 0; i < len; ++i) {        j = i - 1        temp = arr[i]        while (j >= 0 && arr[j] > temp) {          arr[j + 1] = arr[j]          j--        }        arr[j + 1] = temp    }    return arr}arr => {
    const len = arr.length
    let j, temp
    for (let i = 0; i < len; ++i) {
        j = i - 1
        temp = arr[i]
        while (j >= 0 && arr[j] > temp) {
          arr[j + 1] = arr[j]
          j--
        }
        arr[j + 1] = temp
    }
    return arr
}

希爾排序(Shell Sort)

希爾排序,也稱 遞減增量排序算法,是 插入排序 的一種更高效的改進版本。希爾排序是非穩定排序算法。

希爾排序是基於插入排序的以下兩點性質而提出改進方法的:

  1. 插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到 線性排序 的效率;

  2. 但插入排序一般來說是低效的,因爲插入排序每次只能將數據移動一位。

步長的選擇是希爾排序的重要部分。

只要最終步長爲1任何步長序列都可以工作。

算法最開始以一定的步長進行排序。

然後會繼續以一定步長進行排序,最終算法以步長爲1進行排序。

當步長爲1時,算法變爲普通插入排序,這就保證了數據一定會被排序。

插入排序的算法步驟如下:

  1. 定義一個用來分割的步長;

  2. 按步長的長度K,對數組進行K趟排序;

  3. 不斷重複上述步驟。

圖示如下:

640?wx_fmt=gif

具體實現如下:


 
const shellSort = arr => {    let gaps = [5, 3, 1] // 定義步長以及分割次數    let len = arr.length    for (let g = 0, gLen = gaps.length; g < gaps.length; ++g) {        for (let i = gaps[g]; i < len; ++i) {            let temp = arr[i], j            for (j = i; j >= gaps[g] && arr[j - gaps[g]] > arr[i]; j -= gaps[g]) {                arr[j] = arr[j - gaps[g]]            }            arr[j] = temp        }    }    return arr}arr => {
    let gaps = [531// 定義步長以及分割次數
    let len = arr.length
    for (let g = 0, gLen = gaps.length; g < gaps.length; ++g) {
        for (let i = gaps[g]; i < len; ++i) {
            let temp = arr[i], j
            for (j = i; j >= gaps[g] && arr[j - gaps[g]] > arr[i]; j -= gaps[g]) {
                arr[j] = arr[j - gaps[g]]
            }
            arr[j] = temp
        }
    }
    return arr
}

快速排序(Quick Sort)

快速排序(Quicksort),又稱 劃分交換排序(partition-exchange sort) 。

快速排序(Quicksort) 在平均狀況下,排序 n 個項目要 O(n log n) 次比較。在最壞狀況下則需要 O(n2) 次比較,但這種狀況並不常見。事實上,快速排序 O(n log n) 通常明顯比其他算法更快,因爲它的 內部循環(inner loop) 可以在大部分的架構上很有效率地達成。

快速排序使用 分治法(Divide and conquer) 策略來把一個序列分爲較小和較大的2個子序列,然後遞歸地排序兩個子序列。

快速排序的算法步驟如下:

  1. 挑選基準值:從數列中挑出一個元素,稱爲 “基準”(pivot) ;

  2. 分割:重新排序序列,所有比基準值小的元素擺放在基準前面,所有比基準值大的元素擺在基準後面(與基準值相等的數可以到任何一邊)。在這個分割結束之後,對基準值的排序就已經完成;

  3. 遞歸排序子序列:遞歸地將小於基準值元素的子序列和大於基準值元素的子序列排序。

遞歸到最底部的判斷條件是序列的大小是零或一,此時該數列顯然已經有序。

選取基準值有數種具體方法,此選取方法對排序的時間性能有決定性影響。

圖示如下:

640?wx_fmt=gif

具體實現如下:

const quickSort = arr => {    const len = arr.length    if (len < 2) {        return arr    }    const pivot = arr[0]    const left = []    const right = []    for (let i = 1; i < len; ++i) {        if (arr[i] >= pivot) {            right.push(arr[i])        }        if (arr[i] < pivot) {            left.push(arr[i])        }    }    return [...quickSort(left), pivot, ...quickSort(right)]}arr => {
    const len = arr.length
    if (len < 2) {
        return arr
    }
    const pivot = arr[0]
    const left = []
    const right = []
    for (let i = 1; i < len; ++i) {
        if (arr[i] >= pivot) {
            right.push(arr[i])
        }
        if (arr[i] < pivot) {
            left.push(arr[i])
        }
    }
    return [...quickSort(left), pivot, ...quickSort(right)]
}

除了常規的快速排序之外,還有一個快速排序的優化版本,叫 三路快排

當面對一個有大量重複的數據的序列時,選取 pivot 的快速排序有可能會退化成一個 O(n²) 的算法

640?wx_fmt=jpeg

基於這種情況,就有了 三路快排(3 Ways Quick Sort)

三路快排就是將序列分爲三部分:小於pivot,等於 pivot 和大於 pivot,之後遞歸的對小於v和大於v部分進行排序。

640?wx_fmt=png

具體實現如下:

const quickSort = arr => {    const len = arr.length    if (len < 2) {        return arr    }    let left = []    let center = []    let right = []    let pivot = arr[0]    for (let i = 0; i < len; ++i) {              if (arr[i] < pivot) {            left.push(arr[i])        } else if (arr[i] === pivot) {            center.push(arr[i])        } else {            right.push(arr[i])        }    }    return [...quickSort(left), ...center, ...quickSort(right)]}arr => {
    const len = arr.length
    if (len < 2) {
        return arr
    }
    let left = []
    let center = []
    let right = []
    let pivot = arr[0]
    for (let i = 0; i < len; ++i) {      
        if (arr[i] < pivot) {
            left.push(arr[i])
        } else if (arr[i] === pivot) {
            center.push(arr[i])
        } else {
            right.push(arr[i])
        }
    }
    return [...quickSort(left), ...center, ...quickSort(right)]
}

辛苦你們看到這裏了,如果累了可以收藏起來下次再看,不過收藏起來就沒下次了吧?所以繼續看完吧!加油鴨~

並歸排序(Merge Sort)

歸併排序(Merge sort) ,是創建在歸併操作上的一種有效的排序算法,時間複雜度爲 O(n log n) 。1945年由約翰·馮·諾伊曼首次提出。該算法是採用 分治法(Divide and Conquer) 的一個非常典型的應用,且各層分治遞歸可以同時進行。

其實說白了就是將兩個已經排序的序列合併成一個序列的操作。

並歸排序有兩種實現方式

第一種是 自上而下的遞歸 ,算法步驟如下:

  1. 申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列;

  2. 設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置;

  3. 比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置;

  4. 重複步驟3直到某一指針到達序列尾;

  5. 將另一序列剩下的所有元素直接複製到合併序列尾。

具體實現如下:

const merge = (left, right) => {    let resArr = []    while (left.length && right.length) {        if (left[0] < right[0]) {            resArr.push(left.shift())        } else {            resArr.push(right.shift())        }    }    return resArr.concat(left, right)}const mergeSort = arr => {    if (arr.length <= 1) {        return arr    }    let middle = Math.floor(arr.length / 2)    let left = arr.slice(0, middle)    let right = arr.slice(middle)    return merge(mergeSort(left), mergeSort(right))}(left, right) => {
    let resArr = []
    while (left.length && right.length) {
        if (left[0] < right[0]) {
            resArr.push(left.shift())
        } else {
            resArr.push(right.shift())
        }
    }
    return resArr.concat(left, right)
}

const mergeSort = arr => {
    if (arr.length <= 1) {
        return arr
    }
    let middle = Math.floor(arr.length / 2)
    let left = arr.slice(0, middle)
    let right = arr.slice(middle)
    return merge(mergeSort(left), mergeSort(right))
}

第二種是 自下而上的迭代 ,由於 分治法 的具體算法基本都能用 遞歸 跟 迭代 來實現,所有才有這種寫法,其主要步驟如下:

  1. 將序列每相鄰兩個數字進行 歸併操作 ,形成 ceil(n / 2) 個序列,排序後每個序列包含兩/一個元素;

  2. 若此時序列數不是1個則將上述序列再次歸併,形成 ceil(n / 4)  個序列,每個序列包含四/三個元素;

  3. 重複步驟2,直到所有元素排序完畢,即序列數爲1。

具體實現如下:

const merge = (arr, startLeft, stopLeft, startRight, stopRight) => {    /* 建立左右子序列 */    let rightArr = new Array(stopRight - startRight + 1)    let leftArr = new Array(stopLeft - startLeft + 1)    /* 給左右序列排序 */    let k = startRight    for (let i = 0, len = rightArr.length; i < len - 1; ++i) {        rightArr[i] = arr[k]        ++k    }    k = startLeft    for (let i = 0, len = leftArr.length; i < len - 1; ++i) {        leftArr[i] = arr[k]        ++k    }    //設置哨兵值,當左子列或右子列讀取到最後一位時,即Infinity,可以讓另一個剩下的列中的值直接插入到數組中    rightArr[rightArr.length - 1] = Infinity    leftArr[leftArr.length - 1] = Infinity    let m = 0    let n = 0    // 比較左子列和右子列第一個值的大小,小的先填入數組,接着再進行比較    for (let c = startLeft; c < stopRight; ++c) {        if (leftArr[m] <= rightArr[n]) {            arr[c] = leftArr[m]            m++        } else {            arr[c] = rightArr[n]            n++        }    }}const mergeSort = arr => {    if (arr.length <= 1) {        return arr    }    //設置子序列的大小    let step = 1    let left    let right    while (step < arr.length) {        left = 0        right = step        while (right + step <= arr.length) {            merge(arr, left, left + step, right, right + step)            left = right + step            right = left + step        }        if (right < arr.length) {            merge(arr, left, left + step, right, arr.length)        }        step *= 2    }    return arr}(arr, startLeft, stopLeft, startRight, stopRight) => {
    /* 建立左右子序列 */
    let rightArr = new Array(stopRight - startRight + 1)
    let leftArr = new Array(stopLeft - startLeft + 1)
    /* 給左右序列排序 */
    let k = startRight
    for (let i = 0, len = rightArr.length; i < len - 1; ++i) {
        rightArr[i] = arr[k]
        ++k
    }
    k = startLeft
    for (let i = 0, len = leftArr.length; i < len - 1; ++i) {
        leftArr[i] = arr[k]
        ++k
    }
    //設置哨兵值,當左子列或右子列讀取到最後一位時,即Infinity,可以讓另一個剩下的列中的值直接插入到數組中
    rightArr[rightArr.length - 1] = Infinity
    leftArr[leftArr.length - 1] = Infinity
    let m = 0
    let n = 0
    // 比較左子列和右子列第一個值的大小,小的先填入數組,接着再進行比較
    for (let c = startLeft; c < stopRight; ++c) {
        if (leftArr[m] <= rightArr[n]) {
            arr[c] = leftArr[m]
            m++
        } else {
            arr[c] = rightArr[n]
            n++
        }
    }
}
const mergeSort = arr => {
    if (arr.length <= 1) {
        return arr
    }
    //設置子序列的大小
    let step = 1
    let left
    let right
    while (step < arr.length) {
        left = 0
        right = step
        while (right + step <= arr.length) {
            merge(arr, left, left + step, right, right + step)
            left = right + step
            right = left + step
        }
        if (right < arr.length) {
            merge(arr, left, left + step, right, arr.length)
        }
        step *= 2
    }
    return arr
}

魚頭注:迭代比起遞歸還是安全很多,太深的遞歸容易導致堆棧溢出。

圖示如下:

640?wx_fmt=gif

堆排序(Heap Sort)

堆排序(Heapsort) 是指利用 二叉堆 這種數據結構所設計的一種排序算法。堆是一個近似 完全二叉樹 的結構,並同時滿足 堆積的性質 :即子節點的鍵值或索引總是小於(或者大於)它的父節點。

二叉堆是什麼?

二叉堆分以下兩個類型:

魚頭注:以下圖片來自於:漫畫:什麼是二叉堆?

1.最大堆:最大堆任何一個父節點的值,都大於等於它左右孩子節點的值。

2.最小堆:最小堆任何一個父節點的值,都小於等於它左右孩子節點的值。

堆排序的算法步驟如下:

  1. 把無序數列構建成二叉堆;

  2. 循環刪除堆頂元素,替換到二叉堆的末尾,調整堆產生新的堆頂。

具體實現如下:

/* 堆下沉調整 */const adjustHeap = (arr, parentIndex, length) => {    let temp = arr[parentIndex] /* temp保存父節點值,用於最後賦值 */    let childIndex = 2 * parentIndex + 1 /* 保存子節點位置 */    while (childIndex < length) {        /* 如果有右子節點,且右子節點大於左子節點的值,則定位到右子節點 */        if (childIndex + 1 < length && arr[childIndex + 1] > arr[childIndex]) {            childIndex++        }        /* 如果父節點小於任何一個子節點的值,直接退出循環 */        if (temp >= arr[childIndex]) {            break;        }        /* 無序交換,單向賦值即可 */        arr[parentIndex] = arr[childIndex]        parentIndex = childIndex        childIndex = 2 * childIndex + 1    }    arr[parentIndex] = temp}const heapSort = arr => {    /* 把無序數列構建成最大堆 */    for (let i = Math.floor(arr.length / 2); i >= 0; --i) {        adjustHeap(arr, i, arr.length - 1)    }    for (let i = arr.length - 1; i > 0; --i) {        /* 交換最後一個元素與第一個元素 */        [arr[i], arr[0]] = [arr[0], arr[i]]        /* 調整最大堆 */        adjustHeap(arr, 0, i)    }    return arr}
const adjustHeap = (arr, parentIndex, length) => {
    let temp = arr[parentIndex] /* temp保存父節點值,用於最後賦值 */
    let childIndex = 2 * parentIndex + 1 /* 保存子節點位置 */
    while (childIndex < length) {
        /* 如果有右子節點,且右子節點大於左子節點的值,則定位到右子節點 */
        if (childIndex + 1 < length && arr[childIndex + 1] > arr[childIndex]) {
            childIndex++
        }
        /* 如果父節點小於任何一個子節點的值,直接退出循環 */
        if (temp >= arr[childIndex]) {
            break;
        }
        /* 無序交換,單向賦值即可 */
        arr[parentIndex] = arr[childIndex]
        parentIndex = childIndex
        childIndex = 2 * childIndex + 1
    }
    arr[parentIndex] = temp
}
const heapSort = arr => {
    /* 把無序數列構建成最大堆 */
    for (let i = Math.floor(arr.length / 2); i >= 0; --i) {
        adjustHeap(arr, i, arr.length - 1)
    }
    for (let i = arr.length - 1; i > 0; --i) {
        /* 交換最後一個元素與第一個元素 */
        [arr[i], arr[0]] = [arr[0], arr[i]]
        /* 調整最大堆 */
        adjustHeap(arr, 0, i)
    }
    return arr
}

圖示如下:

640?wx_fmt=gif

圖片來源於:CSDN

計數排序(Counting Sort)

計數排序(Counting sort) 是一種穩定的線性時間排序算法。該算法於1954年由 Harold H. Seward 提出。計數排序使用一個額外的數組來存儲輸入的元素,計數排序要求輸入的數據必須是有確定範圍的整數。

當輸入的元素是 n 個 0 到 k 之間的整數時,它的運行時間是 O(n + k) 。計數排序不是比較排序,排序的速度快於任何比較排序算法。

計數排序的算法步驟如下:

  1. 找出待排序的數組中最大和最小的元素;

  2. 統計數組中每個值爲 i 的元素出現的次數,存入數組 C 的第 i 項;

  3. 對所有的計數累加(從數組 C 中的第一個元素開始,每一項和前一項相加);

  4. 反向填充目標數組:將每個元素 i 放在新數組的第 C[i] 項,每放一個元素就將 C[i] 減去1。

具體實現如下:

const countSort = arr => {    const C = []    for (let i = 0, iLen = arr.length; i < iLen; ++i) {        const j = arr[i]        if (C[j] >= 1) {            C[j]++        } else {            C[j] = 1        }    }    const D = []    for (let j = 0, jLen = C.length; j < jLen; ++j) {        if (C[j]) {            while (C[j] > 0) {                D.push(j)                C[j]--            }        }    }    return D}arr => {
    const C = []
    for (let i = 0, iLen = arr.length; i < iLen; ++i) {
        const j = arr[i]
        if (C[j] >= 1) {
            C[j]++
        } else {
            C[j] = 1
        }
    }
    const D = []
    for (let j = 0, jLen = C.length; j < jLen; ++j) {
        if (C[j]) {
            while (C[j] > 0) {
                D.push(j)
                C[j]--
            }
        }
    }
    return D
}

圖示如下:

640?wx_fmt=gif

桶排序(Bucket Sort)

桶排序(Bucket Sort) 跟 計數排序(Counting sort) 一樣是一種穩定的線性時間排序算法,不過這次需要的輔助不是計數,而是桶。

工作的原理是將數列分到有限數量的桶裏。每個桶再個別排序。當要被排序的數組內的數值是均勻分配的時候,桶排序使用線性時間 O(n)

桶排序的算法步驟如下:

  1. 設置一個定量的數組當作空桶子;

  2. 尋訪序列,並且把項目一個一個放到對應的桶子去;

  3. 對每個不是空的桶子進行排序;

  4. 從不是空的桶子裏把項目再放回原來的序列中。

具體實現如下:

const bucketSort = arr => {    let bucketsCount = 10 /* 默認桶的數量 */    const max = Math.max(...arr) /* 序列最大數字 */    const min = Math.min(...arr) /* 數列最小數字 */    const bucketsSize = Math.floor((max - min) / bucketsCount) + 1 /* 桶的深度 */    const __buckets = [] /* 空桶 */    for (let i = 0, len = arr.length; i < len; ++i) {        const index = ~~(arr[i] / bucketsSize) /* 騷操作,取數列中最大或最小的序列 */        if (!__buckets[index]) {            __buckets[index] = [] /* 創建子桶 */        }        __buckets[index].push(arr[i])        let bLen = __buckets[index].length        while (bLen > 0) { /* 子桶排序 */            if (__buckets[index][bLen] < __buckets[index][bLen - 1]) {                [__buckets[index][bLen], __buckets[index][bLen - 1]] = [__buckets[index][bLen - 1], __buckets[index][bLen]]            }            bLen--        }    }    let buckets = [] /* 真實序列 */    for (let i = 0, len = __buckets.length; i < len; ++i) {        if (__buckets[i]) {            buckets.push(...__buckets[i])        }    }    return buckets}arr => {
    let bucketsCount = 10 /* 默認桶的數量 */
    const max = Math.max(...arr) /* 序列最大數字 */
    const min = Math.min(...arr) /* 數列最小數字 */
    const bucketsSize = Math.floor((max - min) / bucketsCount) + 1 /* 桶的深度 */
    const __buckets = [] /* 空桶 */
    for (let i = 0, len = arr.length; i < len; ++i) {
        const index = ~~(arr[i] / bucketsSize) /* 騷操作,取數列中最大或最小的序列 */
        if (!__buckets[index]) {
            __buckets[index] = [] /* 創建子桶 */
        }
        __buckets[index].push(arr[i])
        let bLen = __buckets[index].length
        while (bLen > 0) { /* 子桶排序 */
            if (__buckets[index][bLen] < __buckets[index][bLen - 1]) {
                [__buckets[index][bLen], __buckets[index][bLen - 1]] = [__buckets[index][bLen - 1], __buckets[index][bLen]]
            }
            bLen--
        }
    }
    let buckets = [] /* 真實序列 */
    for (let i = 0, len = __buckets.length; i < len; ++i) {
        if (__buckets[i]) {
            buckets.push(...__buckets[i])
        }
    }
    return buckets
}

圖示如下:

640?wx_fmt=gif

 

圖片來源於:CSDN

基數排序(Radix Sort)

基數排序(Radix sort) 是一種非比較型整數排序算法,其原理是將整數按位數切割成不同的數字,然後按每個位數分別比較。由於整數也可以表達字符串(比如名字或日期)和特定格式的浮點數,所以基數排序也不是隻能使用於整數。

工作原理是將所有待比較數值(正整數)統一爲同樣的數字長度,數字較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後,數列就變成一個有序序列。

基數排序的方式可以採用 LSD(Least significant digital) 或 MSD(Most significant digital) 。

LSD 的排序方式由鍵值的 最右邊(最小位) 開始,而 MSD 則相反,由鍵值的 最左邊(最大位) 開始。

MSD 方式適用於位數多的序列。

LSD 方式適用於位數少的序列。

基數排序 、 桶排序 、 計數排序 原理都差不多,都藉助了 “桶” 的概念,但是使用方式有明顯的差異,其差異如下:

LSD 圖示如下:

640?wx_fmt=gif

LSD 實現如下:

const LSDRadixSort = arr => {    const max = Math.max(...arr) /* 獲取最大值 */    let digit = `${max}`.length /* 獲取最大值位數 */    let start = 1 /* 桶編號 */    let buckets = [] /* 空桶 */    while (digit > 0) {        start *= 10        /* 入桶 */        for (let i = 0, len = arr.length; i < len; ++i) {            const index = (arr[i] % start)            if (!buckets[index]) {                buckets[index] = []            }            buckets[index].push(arr[i]) /* 往不同桶裏添加數據 */        }        arr = []        /* 出桶 */        for(let i = 0; i < buckets.length; i++) {            if (buckets[i]) {                arr = arr.concat(buckets[i])            }        }        buckets = []        digit --    }    return arr}arr => {
    const max = Math.max(...arr) /* 獲取最大值 */
    let digit = `${max}`.length /* 獲取最大值位數 */
    let start = 1 /* 桶編號 */
    let buckets = [] /* 空桶 */
    while (digit > 0) {
        start *= 10
        /* 入桶 */
        for (let i = 0, len = arr.length; i < len; ++i) {
            const index = (arr[i] % start)
            if (!buckets[index]) {
                buckets[index] = []
            }
            buckets[index].push(arr[i]) /* 往不同桶裏添加數據 */
        }
        arr = []
        /* 出桶 */
        for(let i = 0; i < buckets.length; i++) {
            if (buckets[i]) {
                arr = arr.concat(buckets[i])
            }
        }
        buckets = []
        digit --
    }
    return arr
}

特別的冒泡排序——雞尾酒排序(Cocktail Sort)

雞尾酒排序,是 冒泡排序 的一種變形。此算法與 冒泡排序 不同的地方在於從低到高然後從高到低,而 冒泡排序 則僅從低到高去比較序列裏的每個元素。它可以得到比 冒泡排序 稍微好一點的性能,原因是 冒泡排序 只從一個方向進行比對(由低到高),每次循環只移動一個項目。

算法步驟如下:

  1. 步驟跟冒泡算法差不多,區別在於從起點到終點遍歷完之後會進行一次終點到起點的遍歷。

圖示如下:

640?wx_fmt=gif

具體實現如下:

const cocktailSort = arr => {    let i    let left = 0    let right = arr.length - 1    while (left < right) {        for (i = left; i < right; ++i)            if (arr[i] > arr[i + 1]) {                [arr[i], arr[i + 1]] = [arr[i + 1], arr[i]]            }        right--        for (i = right; i > left; --i)            if (arr[i - 1] > arr[i]) {                [arr[i], arr[i - 1]] = [arr[i - 1], arr[i]]            }        left++    }    return arr}arr => {
    let i
    let left = 0
    let right = arr.length - 1
    while (left < right) {
        for (i = left; i < right; ++i)
            if (arr[i] > arr[i + 1]) {
                [arr[i], arr[i + 1]] = [arr[i + 1], arr[i]]
            }
        right--
        for (i = right; i > left; --i)
            if (arr[i - 1] > arr[i]) {
                [arr[i], arr[i - 1]] = [arr[i - 1], arr[i]]
            }
        left++
    }
    return arr
}

不正經的排序——睡眠排序(Sleep Sort)

這種排序算法是基於定時器來實現的,時間複雜度Mmmmmmmmmmm,空間複雜度Mmmmmmmmm,沒有圖示,具體實現如下:

const list = [3, 4, 5, 8, 9, 7, 1, 3, 4, 3, 6]const newList = []list.forEach(item => {    setTimeout(function () {        newList.push(item)    }, item * 100)})34589713436]
const newList = []
list.forEach(item => {
    setTimeout(function () {
        newList.push(item)
    }, item * 100)
})

實用性爲0,純屬娛樂,哈哈哈哈~

640?wx_fmt=gif

後記

其實排序算法不只有以上幾種,如果全部列出就不是一篇文章可以寫得完的,具體的還需要各位自己去學習,探索。

算法的重要性是不言而喻的,雖然我們在日常生活中不一定會用得上怎樣深奧的算法,但是或多或少都會接觸到有,而且對於一些複雜的業務,算法思維往往能給我們不一樣的靈感,更重要一點就是現在如果我們出去面試,算法也是一個繞不開的考點。

本篇內容只是算法這個話題的入門知識點,更多的歡迎大家深入探索,有興趣的也可以加魚頭的微信 “krisChans95” 一起探討。

 

如果你、喜歡探討技術,或者對本文有任何的意見或建議,你可以掃描下方二維碼,關注微信公衆號“魚頭的Web海洋”,隨時與魚頭互動。歡迎!衷心希望可以遇見你。

640?wx_fmt=png

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章