手撕排序算法(JavaScript 實現)

前言

js

俗話說金三銀四 金九銀十,立刻又到了求職跳槽的黃金季。可是今年的這種大環境下,前端崗位的競爭勢必比往日更加激烈。javascript

在現在的面試過程當中,算法是經常被考察的知識點,而排序做爲算法中比較基礎的部分,被面試官要求當場手寫幾種排序算法也不算是過度的要求。html

因此最近將十種常見的排序算法整理以下,並附上一些常見的優化方法以及一些對應的leetcode(傳送門) 題目,建議你們能夠申請個帳號刷起來,畢竟看明白了跟可以寫出來而且經過LeetCode全部的 case 是兩碼事😂,但願能夠對剛接觸算法以及最近須要參加面試的小夥伴有一點幫助。前端

畢竟手裏有糧 內心不慌(逃~java

  • 本篇將主要講解如下十個經典排序算法:
    • 冒泡排序
    • 選擇排序
    • 插入排序
    • 歸併排序
    • 堆排序
    • 快速排序
    • 桶排序
    • 基數排序
    • 希爾排序
    • 計數排序

想看源碼戳這裏,讀者能夠 Clone 下來本地跑一下。BTW,文章配合源碼體驗更棒哦~~~git

最後,限於我的能力,如過在閱讀過程當中遇到問題或有更好的優化方法,能夠:github

  • 提issue給我
  • 或是pull requests
  • 在本篇下評論

我都會看到並處理,歡迎Star,點贊,您的支持是我寫做最大的動力。面試

準備

  • 排序算法的穩定性: 排序先後兩個相等的數相對位置不變,則算法穩定。算法

  • 時間複雜度: 簡單的理解爲一個算法執行所耗費的時間,通常使用大O符號表示法,詳細解釋見時間複雜度api

  • 空間複雜度: 運行完一個程序所需內存的大小。數組

常見算法的複雜度(圖片來源於網絡

複雜度
如下算法最頻繁的操做就是交換數組中兩個元素的位置(按照正序或者是逆序),簡單抽出一個函數以下:

/** * 按照正序比較並交換數組中的兩項 * * @param {Array} ary * @param {*} x * @param {*} y */
function swap(ary, x, y) {
  if (x === y) return
  var temp = ary[x]
  ary[x] = ary[y]
  ary[y] = temp
}
複製代碼

冒泡排序(Bubble-Sort)

冒泡排序是一種簡單的排序算法。它重複地走訪過要排序的數列,一次比較兩個元素,若是他們的順序錯誤就把他們交換過來。走訪數列的工做是重複地進行直到沒有再須要交換,也就是說該數列已經排序完成。這個算法的名字由來是由於越小的元素會經由交換慢慢「浮」到數列的頂端。

冒泡

算法步驟: 假設咱們最終須要的是依次遞增的有序數組

  1. 從數組的第一位開始,依次向後比較相鄰元素的大小,若是前一個比後一個小,那麼交換兩者位置,直至數組末尾。
  2. 下一輪比較的起始位置加1,而後重複第一步。
  3. 重複1~2,直至排序結束。
function bubbleSort1(ary) {
  var l = ary.length
  for (var i = 0; i < l-1; i++) {
    for (var j = 0; j <= l-2; j++) {
      if (ary[j] > ary[j + 1]) {
        swap(ary, j, j + 1)
      }
    }
  }
  return ary
}

複製代碼

優化: 上述排序對於一個長度爲 n 的數組排序須要進行 n * n 次排序。(內外兩層循環次數都是 n ) 能夠預見到的是,每進行一輪冒泡,從數組末尾起有序部分長度就會加一,這就意味着數組末尾的有序數組進行比較的操做是無用的。

改進後的算法以下:

function bubbleSort2(ary) {
  var l = ary.length
  for (var i = l - 1; i >= 0; i--) {
    // 優化的部分 arr[i]及以後的部分都是有序的
    for (var j = 0; j < i; j++) {
      if (ary[j] > ary[j + 1]) {
        swap(ary, j, j + 1)
      }
    }
  }
  return ary
}
複製代碼

優化點:對於一些比較極限狀況的處理,舉一個比較極限的例子,假如給定的數組已是有序數組了,那麼 bubbleSort1 和 bubbleSort2 仍是傻傻的去走完預約的次數 分別爲 n*n 和 n!。 固然這種狀況並不容易遇到,可是在排序的後段部分很容易遇到的是,理論上應該是未排序的部分其實已是有序的了,咱們須要對這種狀況進行甄別並處理。 引入一個 swapedFlag ,若是在排序的上一步沒有進入內層循環,那麼代表剩餘元素都是有序的,排序完成。

優化後的代碼以下:

/** * 冒泡排序 優化 * * @param {Array} ary * @returns */
function bubbleSort3(ary) {
  var l = ary.length
  var swapedFlag
  for (var i = l - 1; i >= 0; i--) {
    swapedFlag = false
    for (var j = 0; j < i; j++) {
      if (ary[j] > ary[j + 1]) {
        swapedFlag = true
        swap(ary, j, j + 1)
      }
    }
    if (!swapedFlag) {
      break
    }
  }
  return ary
}
複製代碼

選擇排序(Selection-Sort)

選擇排序是先在數據中找出最大或最小的元素,放到序列的起始;而後再從餘下的數據中繼續尋找最大或最小的元素,依次放到排序序列中,直到全部數據樣本排序完成。 複雜度分析:很顯然,選擇排序也是一個費時的排序算法,不管什麼數據,都須要O(n*n) 的時間複雜度,不適宜大量數據的排序。

選擇

算法步驟: 初始狀態爲n的無序區(數組)可通過n-1趟直接選擇排序獲得有序結果

  1. 初始狀態:無序區爲R[0..n],有序區爲空;
  2. 第i趟排序(i=0,1,2,3…n-1)開始時,當前有序區和無序區分別爲R[0..i]和R(i+1..n)。該趟排序從當前無序區中選出關鍵字最小的記錄的位置(下標 minPos),將 R[minPos] 與無序區的第1個記錄 R[i] 交換,因此有序區長度加 1 無序區長度減 1 。而後進行 i 加 1 並進行下一趟排序。
  3. n-1趟結束,數組排序完成
function selectSort(ary) {
  var l = ary.length
  var minPos
  for (var i = 0; i < l - 1; i++) {
    minPos = i
    for (var j = i + 1; j < l; j++) {
      if (ary[j] - ary[minPos] < 0) {
        minPos = j
      }
    }
    swap(ary, i, minPos)
  }
  return ary
}
複製代碼

插入排序(Insertion-Sort)

插入排序是先將待排序序列的第一個元素看作一個有序序列,把第二個元素到最後一個元素當成是未排序序列;而後從頭至尾依次掃描未排序序列,將掃描到的每一個元素插入有序序列的適當位置,直到全部數據都完成排序;若是待插入的元素與有序序列中的某個元素相等,則將待插入元素插入到相等元素的後面。

插入
注:動圖對應的是最爲原始的插入排序,沒有在網上找到二分法對應的動圖,你們見諒。

算法步驟:

  1. 從第一個元素開始,該元素能夠認爲已經被排序
  2. 取出下一個元素,在已經排序的元素序列中從後向前掃描
  3. 若是該元素(已排序)大於新元素,將該元素移到下一位置
  4. 重複步驟 3,直到找到已排序的元素小於或者等於新元素的位置
  5. 將新元素插入到該位置後
  6. 重複步驟 2~5
function insertionSort1(arr) {
    var l = arr.length;
    var preIndex, current;
    for (var i = 1; i < l; i++) {
        preIndex = i - 1;
        current = arr[i];
        while (preIndex >= 0 && arr[preIndex] > current) {
            arr[preIndex + 1] = arr[preIndex];
            preIndex--;
        }
        arr[preIndex + 1] = current;
    }
    return arr;
}
複製代碼

優化思路:

  • 二分法:即在將新增的數值插入到有序數組中時,經過二分法減小查找次數。
  • 鏈表:將有序數組部分轉爲鏈表結構,那麼插入的時間複雜度變爲O(1),查找複雜度變爲O(n)(由於不方便使用二分法)
  • 排序二叉樹(BST): 將有序數組部分轉化爲排序二叉樹結構,而後中序遍歷該二叉樹。利用排序二叉樹能夠兼顧插入方便以及查找的效率。可是須要佔用額外空間。因此 BST 是比較平衡的一種思路, 也是空間換時間思路的體現。

簡單介紹下二分法: 二分查找法,是一種在有序數組中查找某一特定元素的搜索算法。搜素過程從數組的中間元素開始,若是中間元素正好是要查找的元素,則搜素過程結束;若是某一特定元素大於或者小於中間元素,則在數組大於或小於中間元素的那一半中查找,並且跟開始同樣從中間元素開始比較。若是在某一步驟數組爲空,則表明找不到。這種搜索算法每一次比較都使搜索範圍縮小一半。

注: 準備面試的同窗可以理解並記憶如下一種便可,排序二叉樹和鏈表的實現限於篇幅就不細說,準備之後寫數據結構時再詳細介紹,本篇介紹下使用二分法優化拆入排序的思路:

/** * 插入排序 * * @param {*} ary * @returns {Arrray} 排序完成的數組 */
function insertSort2(ary) {
  return ary.reduce(insert, [])
}

/** * 使用二分法完成查找插值位置,並完成插值操做。 * 時間複雜度 logN * @param {*} sortAry 有序數組部分 * @param {*} val * @returns */
function insert(sortAry, val) {
  var l = sortAry.length
  if (l == 0) {
    sortAry.push(val)
    return sortAry
  }

  var i = 0,
    j = l,
    mid
  //先判斷是否爲極端值
  if (val < sortAry[i]) {
    return sortAry.unshift(val), sortAry
  }
  if (val >= sortAry[l - 1]) {
    return sortAry.push(val), sortAry
  }

  while (i < j) {
    mid = ((j + i) / 2) | 0
    //結束條件 等價於j - i ==1
    if (i == mid) {
      break
    }
    if (val < sortAry[mid]) {
      j = mid
    }
    if (val == sortAry[mid]) {
      i = mid
      break
    }
    //結束條件 統一c處理對外輸出i
    if (val > sortAry[mid]) {
      i = mid
    }
  }
  var midArray = [val]
  var lastArray = sortAry.slice(i + 1)
  sortAry = sortAry
    .slice(0, i + 1)
    .concat(midArray)
    .concat(lastArray)
  return sortAry
}
複製代碼

歸併排序(Merge-Sort)

歸併排序是利用歸併的思想實現的排序方法,該算法採用經典的分治(divide-and-conquer)策略(分治法將問題分(divide)成一些小的問題而後遞歸求解,而治(conquer)的階段則將分的階段獲得的各答案"修補"在一塊兒,即分而治之)。

穩定性分析:歸併排序嚴格遵循從左到右或從右到左的順序合併子數據序列, 它不會改變相同數據之間的相對順序, 所以歸併排序是一種穩定的排序算法.

歸併排序

算法步驟:

  1. 把長度爲n的輸入序列分紅兩個長度爲n/2的子序列;
  2. 對這兩個子序列分別採用歸併排序;
  3. 將兩個排序好的子序列合併成一個最終的排序序列。
// 採用自上而下的遞歸方法
function mergeSort(ary) {
  if (ary.length < 2) {
    return ary.slice()
  }

  var mid = Math.floor(ary.length / 2)
  var left = mergeSort(ary.slice(0, mid))
  var right = mergeSort(ary.slice(mid))
  var result = []

  while (left.length && right.length) {
    if (left[0] <= right[0]) {
      result.push(left.shift())
    } else {
      result.push(right.shift())
    }
  }

  result.push(...left, ...right)

  return result
}
複製代碼

堆排序(Heapsort)

堆排序是指利用堆這種數據結構所設計的一種排序算法。堆積結構具備以下特色:即子結點的鍵值老是小於(或者大於)它的父節點,據此可分爲如下兩類:

  • 最大堆:每一個節點的值都大於或等於其子節點的值,在堆排序算法中用於升序排列;
  • 最小堆:每一個節點的值都小於或等於其子節點的值,在堆排序算法中用於降序排列;

堆排序

算法步驟:

  1. 建立一個堆 H[0……n-1];
  2. 把堆首(最大值)和堆尾互換;
  3. 把堆的尺寸縮小 1,並調用 reheap 方法從新聚堆,目的是把新的數組頂端數據調整到相應位置,從新變爲最大堆。
/** * 聚堆:將數組中的某一項做爲堆頂,調整爲最大堆。 * 把在堆頂位置的一個可能不是堆,但左右子樹都是堆的樹調整成堆。 * * @param {*} ary 待排序數組 * @param {*} topIndex 當前處理的堆的堆頂 * @param {*} [endIndex=ary.length - 1] 數組的末尾邊界 */
function reheap(ary, topIndex, endIndex = ary.length - 1) {
  if (topIndex > endIndex) {
    return
  }

  var largestIndex = topIndex
  var leftIndex = topIndex * 2 + 1
  var rightIndex = topIndex * 2 + 2

  if (leftIndex <= endIndex && ary[leftIndex] > ary[largestIndex]) {
    largestIndex = leftIndex
  }
  if (rightIndex <= endIndex && ary[rightIndex] > ary[largestIndex]) {
    largestIndex = rightIndex
  }

  if (largestIndex != topIndex) {
    swap(ary, largestIndex, topIndex)
    reheap(ary, largestIndex, endIndex)
  }
}

/** * 將數組調整爲最大堆結構 * * @param {*} ary * @returns */
function heapify(ary) {
  for (var i = ary.length - 1; i >= 0; i--) {
    reheap(ary, i)
  }
  return ary
}

/** * 堆排序 * * @param {*} ary * @returns */
function heapSort(ary) {
  heapify(ary)
  for (var i = ary.length - 1; i >= 1; i--) {
    swap(ary, 0, i)
    reheap(ary, 0, i - 1)
  }
  return ary
}
複製代碼

快速排序(Quicksort)

快速排序使用分治法策略來把一個數組分爲兩個子數組。首先從數組中挑出一個元素,並將這個元素稱爲「基準」,英文pivot。從新排序數組,全部比基準值小的元素擺放在基準前面,全部比基準值大的元素擺在基準後面(相同的數能夠到任何一邊)。在這個分區結束以後,該基準就處於數組的中間位置。這個稱爲分區(partition)操做。以後,在子序列中繼續重複這個方法,直到最後整個數據序列排序完成。

快排

注意: 在 js 中實現快排中最耗費時間的就是交換,本例子中哨兵的元素是隨機取得的,而上面動圖中老是的取數組中的第一個值做爲哨兵(pivot),那麼考慮一種極限狀況,在 [9,8,7,6,5,4,3,2,1] 重中例子中使用就地排序就算法複雜度就會變成 n*n。 本例中的哨兵是從數組中隨機抽取的,我的認爲比取首元素的方案更優。

應用: 取前K大元素、求中位數 、leetcode

嗯,先整一個粗暴版本稍微瞭解下快排的基本思路:

//快排粗暴版本
function quickSort1(ary) {
  if (ary.length < 2) {
    return ary.slice()
  }
  var pivot = ary[Math.floor(Math.random() * ary.length)]
  var left = []
  var middle = []
  var right = []
  for (var i = 0; i < ary.length; i++) {
    var val = ary[i]
    if (val < pivot) {
      left.push(val)
    }
    if (val === pivot) {
      middle.push(val)
    }
    if (val > pivot) {
      right.push(val)
    }
  }

  return quickSort1(left).concat(middle, quickSort(right))
}

複製代碼

這個是推薦掌握的,很重要(敲黑板)

算法步驟

  1. 從數列中挑出一個元素,稱爲 "哨兵"(pivot);
  2. 從新排序數列,全部元素比哨兵值小的擺放在哨兵前面,全部元素比哨兵值大的擺在哨兵的後面(相同的數能夠到任一邊)。在這個分區退出以後,該哨兵就處於數列的中間位置。這個稱爲分區(partition)操做;
  3. 遞歸地把小於哨兵值元素的子數列和大於哨兵值元素的子數列排序。
function quickSort2(ary, comparator = (a, b) => a - b) {
  return partition(ary, comparator)
}
function partition(ary, comparator, start = 0, end = ary.length - 1, ) {
  if (start >= end) {
    return
  }

  var pivotIndex = Math.floor(Math.random() * (end - start + 1) + start)
  var pivot = ary[pivotIndex]

  swap(ary, pivotIndex, end)

  for (var i = start - 1, j = start; j < end; j++) {
    if (comparator(ary[j], pivot) < 0) {
      i++
      swap(ary, i, j)
    }
  }

  swap(ary, i + 1, end)
  partition(ary, comparator, start, i)
  partition(ary, comparator, i + 2, end)
  return ary
}
複製代碼

參考:

  1. javascript-algorithms
  2. 十大經典排序算法(動圖演示)
相關文章
相關標籤/搜索