俗話說金三銀四 金九銀十,立刻又到了求職跳槽的黃金季。可是今年的這種大環境下,前端崗位的競爭勢必比往日更加激烈。javascript
在現在的面試過程當中,算法是經常被考察的知識點,而排序做爲算法中比較基礎的部分,被面試官要求當場手寫幾種排序算法也不算是過度的要求。html
因此最近將十種常見的排序算法整理以下,並附上一些常見的優化方法以及一些對應的leetcode(傳送門) 題目,建議你們能夠申請個帳號刷起來,畢竟看明白了跟可以寫出來而且經過LeetCode全部的 case 是兩碼事😂,但願能夠對剛接觸算法以及最近須要參加面試的小夥伴有一點幫助。前端
畢竟手裏有糧 內心不慌(逃~java
想看源碼戳這裏,讀者能夠 Clone 下來本地跑一下。BTW,文章配合源碼體驗更棒哦~~~git
最後,限於我的能力,如過在閱讀過程當中遇到問題或有更好的優化方法,能夠:github
我都會看到並處理,歡迎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 } 複製代碼
冒泡排序是一種簡單的排序算法。它重複地走訪過要排序的數列,一次比較兩個元素,若是他們的順序錯誤就把他們交換過來。走訪數列的工做是重複地進行直到沒有再須要交換,也就是說該數列已經排序完成。這個算法的名字由來是由於越小的元素會經由交換慢慢「浮」到數列的頂端。
算法步驟: 假設咱們最終須要的是依次遞增的有序數組
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 } 複製代碼
選擇排序是先在數據中找出最大或最小的元素,放到序列的起始;而後再從餘下的數據中繼續尋找最大或最小的元素,依次放到排序序列中,直到全部數據樣本排序完成。 複雜度分析:很顯然,選擇排序也是一個費時的排序算法,不管什麼數據,都須要O(n*n) 的時間複雜度,不適宜大量數據的排序。
算法步驟: 初始狀態爲n的無序區(數組)可通過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 } 複製代碼
插入排序是先將待排序序列的第一個元素看作一個有序序列,把第二個元素到最後一個元素當成是未排序序列;而後從頭至尾依次掃描未排序序列,將掃描到的每一個元素插入有序序列的適當位置,直到全部數據都完成排序;若是待插入的元素與有序序列中的某個元素相等,則將待插入元素插入到相等元素的後面。
算法步驟:
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; } 複製代碼
優化思路:
簡單介紹下二分法: 二分查找法,是一種在有序數組中查找某一特定元素的搜索算法。搜素過程從數組的中間元素開始,若是中間元素正好是要查找的元素,則搜素過程結束;若是某一特定元素大於或者小於中間元素,則在數組大於或小於中間元素的那一半中查找,並且跟開始同樣從中間元素開始比較。若是在某一步驟數組爲空,則表明找不到。這種搜索算法每一次比較都使搜索範圍縮小一半。
注: 準備面試的同窗可以理解並記憶如下一種便可,排序二叉樹和鏈表的實現限於篇幅就不細說,準備之後寫數據結構時再詳細介紹,本篇介紹下使用二分法優化拆入排序的思路:
/** * 插入排序 * * @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 } 複製代碼
歸併排序是利用歸併的思想實現的排序方法,該算法採用經典的分治(divide-and-conquer)策略(分治法將問題分(divide)成一些小的問題而後遞歸求解,而治(conquer)的階段則將分的階段獲得的各答案"修補"在一塊兒,即分而治之)。
穩定性分析:歸併排序嚴格遵循從左到右或從右到左的順序合併子數據序列, 它不會改變相同數據之間的相對順序, 所以歸併排序是一種穩定的排序算法.
算法步驟:
// 採用自上而下的遞歸方法 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 } 複製代碼
堆排序是指利用堆這種數據結構所設計的一種排序算法。堆積結構具備以下特色:即子結點的鍵值老是小於(或者大於)它的父節點,據此可分爲如下兩類:
算法步驟:
/** * 聚堆:將數組中的某一項做爲堆頂,調整爲最大堆。 * 把在堆頂位置的一個可能不是堆,但左右子樹都是堆的樹調整成堆。 * * @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 } 複製代碼
快速排序使用分治法策略來把一個數組分爲兩個子數組。首先從數組中挑出一個元素,並將這個元素稱爲「基準」,英文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)) } 複製代碼
這個是推薦掌握的,很重要(敲黑板)
算法步驟
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 } 複製代碼