本篇有7k+字, 系統梳理了js中常見的12種排序算法。除了基本排序算法,文章還包含了希爾排序、堆排序、桶排序等較爲複雜的排序實現,若是喜歡請點贊支持~謝謝.html
原文: http://louiszhai.github.io/20...git
排序算法能夠稱得上是個人盲點, 曾幾什麼時候當我知道Chrome的Array.prototype.sort使用了快速排序時, 個人心裏是奔潰的(啥是快排, 我只知道冒泡啊?!), 要知道學習一門技術最好的時間是三年前, 希望我如今補習還來得及(捂臉).github
所以本篇重拾了出鏡機率比較高的十來種排序算法, 逐一分析其排序思想, 並批註注意事項. 歡迎對算法提出改進和討論.算法
冒泡排序須要兩個嵌套的循環. 其中, 外層循環
移動遊標; 內層循環
遍歷遊標及以後(或以前)的元素, 經過兩兩交換的方式, 每次只確保該內循環結束位置排序正確, 而後內層循環
週期結束, 交由外層循環
日後(或前)移動遊標, 隨即開始下一輪內層循環
, 以此類推, 直至循環結束.shell
Tips: 因爲冒泡排序只在相鄰元素大小不符合要求時才調換他們的位置, 它並不改變相同元素之間的相對順序, 所以它是穩定的排序算法.windows
因爲有兩層循環, 所以能夠有四種實現方式.數組
方案 | 外層循環 | 內層循環 |
---|---|---|
1 | 正序 | 正序 |
2 | 正序 | 逆序 |
3 | 逆序 | 正序 |
4 | 逆序 | 逆序 |
四種不一樣循環方向, 實現方式略有差別.瀏覽器
以下是動圖效果(對應於方案1: 內/外層循環均是正序遍歷.緩存
以下是上圖的算法實現(對應方案一: 內/外層循環均是正序遍歷).數據結構
//先將交換元素部分抽象出來 function swap(i,j,array){ var temp = array[j]; array[j] = array[i]; array[i] = temp; }
function bubbleSort(array) { var length = array.length, isSwap; for (var i = 0; i < length; i++) { //正序 isSwap = false; for (var j = 0; j < length - 1 - i; j++) { //正序 array[j] > array[j+1] && (isSwap = true) && swap(j,j+1,array); } if(!isSwap) break; } return array; }
以上, 排序的特色就是: 靠後的元素位置先肯定.
方案二: 外循環正序遍歷, 內循環逆序遍歷, 代碼以下:
function bubbleSort(array) { var length = array.length, isSwap; for (var i = 0; i < length; i++) { //正序 isSwap = false; for (var j = length - 1; j >= i+1; j--) { //逆序 array[j] < array[j-1] && (isSwap = true) && swap(j,j-1,array); } if(!isSwap) break; } return array; }
以上, 靠前的元素位置先肯定.
方案三: 外循環逆序遍歷, 內循環正序遍歷, 代碼以下:
function bubbleSort(array) { var length = array.length, isSwap; for (var i = length - 1; i >= 0; i--) { //逆序 isSwap = false; for (var j = 0; j < i; j++) { //正序 array[j] > array[j+1] && (isSwap = true) && swap(j,j+1,array); } if(!isSwap) break; } return array; }
以上, 因爲內循環是正序遍歷, 所以靠後的元素位置先肯定.
方案四: 外循環逆序遍歷, 內循環逆序遍歷, 代碼以下:
function bubbleSort(array) { var length = array.length, isSwap; for (var i = length - 1; i >= 0; i--) { //逆序 isSwap = false; for (var j = length - 1; j >= length - 1 - i; j--) { //逆序 array[j] < array[j-1] && (isSwap = true) && swap(j,j-1,array); } if(!isSwap) break; } return array; }
以上, 因爲內循環是逆序遍歷, 所以靠前的元素位置先肯定.
如下是其算法複雜度:
平均時間複雜度 | 最好狀況 | 最壞狀況 | 空間複雜度 |
---|---|---|---|
O(n²) | O(n) | O(n²) | O(1) |
冒泡排序是最容易實現的排序, 最壞的狀況是每次都須要交換, 共需遍歷並交換將近n²/2次, 時間複雜度爲O(n²). 最佳的狀況是內循環遍歷一次後發現排序是對的, 所以退出循環, 時間複雜度爲O(n). 平均來說, 時間複雜度爲O(n²). 因爲冒泡排序中只有緩存的temp變量須要內存空間, 所以空間複雜度爲常量O(1).
雙向冒泡排序是冒泡排序的一個簡易升級版, 又稱雞尾酒排序. 冒泡排序是從低到高(或者從高到低)單向排序, 雙向冒泡排序顧名思義就是從兩個方向分別排序(一般, 先從低到高, 而後從高到低). 所以它比冒泡排序性能稍好一些.
以下是算法實現:
function bothwayBubbleSort(array){ var tail = array.length-1, i, isSwap = false; for(i = 0; i < tail; tail--){ for(var j = tail; j > i; j--){ //第一輪, 先將最小的數據冒泡到前面 array[j-1] > array[j] && (isSwap = true) && swap(j,j-1,array); } i++; for(j = i; j < tail; j++){ //第二輪, 將最大的數據冒泡到後面 array[j] > array[j+1] && (isSwap = true) && swap(j,j+1,array); } } return array; }
從算法邏輯上看, 選擇排序是一種簡單且直觀的排序算法. 它也是兩層循環. 內層循環
就像工人同樣, 它是真正作事情的, 內層循環
每執行一遍, 將選出本次待排序的元素中最小(或最大)的一個, 存放在數組的起始位置. 而 外層循環
則像老闆同樣, 它告訴內層循環
你須要不停的工做, 直到工做完成(也就是所有的元素排序完成).
Tips: 選擇排序每次交換的元素都有可能不是相鄰的, 所以它有可能打破原來值爲相同的元素之間的順序. 好比數組[2,2,1,3], 正向排序時, 第一個數字2將與數字1交換, 那麼兩個數字2之間的順序將和原來的順序不一致, 雖然它們的值相同, 但它們相對的順序卻發生了變化. 咱們將這種現象稱做 不穩定性
.
以下是動圖效果:
以下是上圖的算法實現:
function selectSort(array) { var length = array.length, min; for (var i = 0; i < length - 1; i++) { min = i; for (var j = i + 1; j < length; j++) { array[j] < array[min] && (min = j); //記住最小數的下標 } min!=i && swap(i,min,array); } return array; }
如下是其算法複雜度:
平均時間複雜度 | 最好狀況 | 最壞狀況 | 空間複雜度 |
---|---|---|---|
O(n²) | O(n²) | O(n²) | O(1) |
選擇排序的簡單和直觀名副其實, 這也造就了它"出了名的慢性子", 不管是哪一種狀況, 哪怕原數組已排序完成, 它也將花費將近n²/2次遍從來確認一遍. 即使是這樣, 它的排序結果也仍是不穩定的. 惟一值得高興的是, 它並不耗費額外的內存空間.
插入排序的設計初衷是往有序的數組中快速插入一個新的元素. 它的算法思想是: 把要排序的數組分爲了兩個部分, 一部分是數組的所有元素(除去待插入的元素), 另外一部分是待插入的元素; 先將第一部分排序完成, 而後再插入這個元素. 其中第一部分的排序也是經過再次拆分爲兩部分來進行的.
插入排序因爲操做不盡相同, 可分爲 直接插入排序
, 折半插入排序
(又稱二分插入排序), 鏈表插入排序
, 希爾排序
.
它的基本思想是: 將待排序的元素按照大小順序, 依次插入到一個已經排好序的數組之中, 直到全部的元素都插入進去.
以下是動圖效果:
以下是上圖的算法實現:
function directInsertionSort(array) { var length = array.length, index, current; for (var i = 1; i < length; i++) { index = i - 1; //待比較元素的下標 current = array[i]; //當前元素 while(index >= 0 && array[index] > current) { //前置條件之一:待比較元素比當前元素大 array[index+1] = array[index]; //將待比較元素後移一位 index--; //遊標前移一位 //console.log(array); } if(index+1 != i){ //避免同一個元素賦值給自身 array[index+1] = current; //將當前元素插入預留空位 //console.log(array); } } return array; }
爲了更好的觀察到直接插入排序的實現過程, 咱們不妨將上述代碼中的註釋部分加入. 以數組 [5,4,3,2,1] 爲例, 以下即是原數組的演化過程.
可見, 數組的各個元素, 從後往前, 只要比前面的元素小, 都依次插入到了合理的位置.
Tips: 因爲直接插入排序每次只移動一個元素的位置, 並不會改變值相同的元素之間的排序, 所以它是一種穩定排序.
折半插入排序是直接插入排序的升級版. 鑑於插入排序第一部分爲已排好序的數組, 咱們沒必要按順序依次尋找插入點, 只需比較它們的中間值與待插入元素的大小便可.
Tips: 同直接插入排序相似, 折半插入排序每次交換的是相鄰的且值爲不一樣的元素, 它並不會改變值相同的元素之間的順序. 所以它是穩定的.
算法基本思想是:
取0 ~ i-1的中間點( m = (i-1)>>1
), array[i] 與 array[m] 進行比較, 若array[i] < array[m] , 則說明待插入的元素array[i] 應該處於數組的 0 ~ m 索引之間; 反之, 則說明它應該處於數組的 m ~ i-1 索引之間.
重複步驟1, 每次縮小一半的查找範圍, 直至找到插入的位置.
將數組中插入位置以後的元素所有後移一位.
在指定位置插入第 i 個元素.
注:
x>>1
是位運算中的右移運算, 表示右移一位, 等同於x除以2再取整, 即x>>1 == Math.floor(x/2)
.
以下是算法實現:
function binaryInsertionSort(array){ var current, i, j, low, high, m; for(i = 1; i < array.length; i++){ low = 0; high = i - 1; current = array[i]; while(low <= high){ //步驟1&2:折半查找 m = (low + high)>>1; if(array[i] >= array[m]){//值相同時, 切換到高半區,保證穩定性 low = m + 1; //插入點在高半區 }else{ high = m - 1; //插入點在低半區 } } for(j = i; j > low; j--){ //步驟3:插入位置以後的元素所有後移一位 array[j] = array[j-1]; } array[low] = current; //步驟4:插入該元素 } return array; }
爲了便於對比, 一樣以數組 [5,4,3,2,1] 舉例?. 原數組的演化過程以下(與上述同樣):
雖然折半插入排序明顯減小了查詢的次數, 可是數組元素移動的次數卻沒有改變. 它們的時間複雜度都是O(n²).
希爾排序也稱縮小增量排序, 它是直接插入排序的另一個升級版, 實質就是分組插入排序. 希爾排序以其設計者希爾(Donald Shell)的名字命名, 並於1959年公佈.
算法的基本思想:
將數組拆分爲若干個子分組, 每一個分組由相距必定"增量"的元素組成. 比方說將[0,1,2,3,4,5,6,7,8,9,10]的數組拆分爲"增量"爲5的分組, 那麼子分組分別爲 [0,5], [1,6], [2,7], [3,8], [4,9] 和 [5,10].
而後對每一個子分組應用直接插入排序.
逐步減少"增量", 重複步驟1,2.
直至"增量"爲1, 這是最後一個排序, 此時的排序, 也就是對全數組進行直接插入排序.
以下是排序的示意圖:
可見, 希爾排序實際上就是不斷的進行直接插入排序, 分組是爲了先將局部元素有序化. 由於直接插入排序在元素基本有序的狀態下, 效率很是高. 而希爾排序呢, 經過先分組後排序的方式, 製造了直接插入排序高效運行的場景. 所以希爾排序效率更高.
咱們試着抽象出共同點, 便不難發現上述希爾排序的第四步就是一次直接插入排序, 而希爾排序本來就是從"增量"爲n開始, 直至"增量"爲1, 循環應用直接插入排序的一種封裝. 所以直接插入排序就能夠看作是步長爲1的希爾排序. 爲此咱們先來封裝下直接插入排序.
//形參增長步數gap(實際上就至關於gap替換了原來的數字1) function directInsertionSort(array, gap) { gap = (gap == undefined) ? 1 : gap; //默認從下標爲1的元素開始遍歷 var length = array.length, index, current; for (var i = gap; i < length; i++) { index = i - gap; //待比較元素的下標 current = array[i]; //當前元素 while(index >= 0 && array[index] > current) { //前置條件之一:待比較元素比當前元素大 array[index + gap] = array[index]; //將待比較元素後移gap位 index -= gap; //遊標前移gap位 } if(index + gap != i){ //避免同一個元素賦值給自身 array[index + gap] = current; //將當前元素插入預留空位 } } return array; }
那麼希爾排序的算法實現以下:
function shellSort(array){ var length = array.length, gap = length>>1, current, i, j; while(gap > 0){ directInsertionSort(array, gap); //按指定步長進行直接插入排序 gap = gap>>1; } return array; }
一樣以數組[5,4,3,2,1] 舉例?. 原數組的演化過程以下:
對比上述直接插入排序和折半插入排序, 數組元素的移動次數由14次減小爲7次. 經過拆分原數組爲粒度更小的子數組, 希爾排序進一步提升了排序的效率.
不只如此, 以上步長設置爲了 {N/2, (N/2)/2, ..., 1}. 該序列即希爾增量, 其它的增量序列 還有Hibbard:{1, 3, ..., 2^k-1}. 經過合理調節步長, 還能進一步提高排序效率. 實際上已知的最好步長序列是由Sedgewick提出的(1, 5, 19, 41, 109,…). 該序列中的項或者是9*4^i - 9*2^i + 1或者是4^i - 3*2^i + 1. 具體請戳 希爾排序-維基百科 .
Tips: 咱們知道, 單次直接插入排序是穩定的, 它不會改變相同元素之間的相對順序, 但在屢次不一樣的插入排序過程當中, 相同的元素可能在各自的插入排序中移動, 可能致使相同元素相對順序發生變化. 所以, 希爾排序並不穩定.
歸併排序創建在歸併操做之上, 它採起分而治之的思想, 將數組拆分爲兩個子數組, 分別排序, 最後纔將兩個子數組合並; 拆分的兩個子數組, 再繼續遞歸拆分爲更小的子數組, 進而分別排序, 直到數組長度爲1, 直接返回該數組爲止.
Tips: 歸併排序嚴格按照從左往右(或從右往左)的順序去合併子數組, 它並不會改變相同元素之間的相對順序, 所以它也是一種穩定的排序算法.
以下是動圖效果:
歸併排序可經過兩種方式實現:
自上而下的遞歸
自下而上的迭代
以下是算法實現(方式1:遞歸):
function mergeSort(array) { //採用自上而下的遞歸方法 var length = array.length; if(length < 2) { return array; } var m = (length >> 1), left = array.slice(0, m), right = array.slice(m); //拆分爲兩個子數組 return merge(mergeSort(left), mergeSort(right));//子數組繼續遞歸拆分,而後再合併 } function merge(left, right){ //合併兩個子數組 var result = []; while (left.length && right.length) { var item = left[0] <= right[0] ? left.shift() : right.shift();//注意:判斷的條件是小於或等於,若是隻是小於,那麼排序將不穩定. result.push(item); } return result.concat(left.length ? left : right); }
由上, 長度爲n的數組, 最終會調用mergeSort函數2n-1次. 經過自上而下的遞歸實現的歸併排序, 將存在堆棧溢出的風險. 親測各瀏覽器的堆棧溢出所需的遞歸調用次數大體爲:
Chrome v55: 15670
Firefox v50: 44488
Safari v9.1.2: 50755
如下是測試代碼:
function computeMaxCallStackSize() { try { return 1 + computeMaxCallStackSize(); } catch (e) { // Call stack overflow return 1; } } var time = computeMaxCallStackSize(); console.log(time);
爲此, ES6規範中提出了尾調優化的思想: 若是一個函數的最後一步也是一個函數調用, 那麼該函數所須要的棧空間將被釋放, 它將直接進入到下次調用中, 最終調用棧裏只保留最後一次的調用記錄.
雖然ES6規範如此誘人, 然而目前並無瀏覽器支持尾調優化, 相信在不久的未來, 尾調優化就會獲得主流瀏覽器的支持.
如下是其算法複雜度:
平均時間複雜度 | 最好狀況 | 最壞狀況 | 空間複雜度 |
---|---|---|---|
O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(n) |
從效率上看, 歸併排序可算是排序算法中的"佼佼者". 假設數組長度爲n, 那麼拆分數組共需logn步, 又每步都是一個普通的合併子數組的過程, 時間複雜度爲O(n), 故其綜合時間複雜度爲O(nlogn). 另外一方面, 歸併排序屢次遞歸過程當中拆分的子數組須要保存在內存空間, 其空間複雜度爲O(n).
快速排序借用了分治的思想, 而且基於冒泡排序作了改進. 它由C. A. R. Hoare在1962年提出. 它將數組拆分爲兩個子數組, 其中一個子數組的全部元素都比另外一個子數組的元素小, 而後對這兩個子數組再重複進行上述操做, 直到數組不可拆分, 排序完成.
以下是動圖效果:
以下是算法實現:
function quickSort(array, left, right) { var partitionIndex, left = typeof left == 'number' ? left : 0, right = typeof right == 'number' ? right : array.length-1; if (left < right) { partitionIndex = partition(array, left, right);//切分的基準值 quickSort(array, left, partitionIndex-1); quickSort(array, partitionIndex+1, right); } return array; } function partition(array, left ,right) { //分區操做 for (var i = left+1, j = left; i <= right; i++) {//j是較小值存儲位置的遊標 array[i] < array[left] && swap(i, ++j, array);//以第一個元素爲基準 } swap(left, j, array); //將第一個元素移至中間 return j; }
如下是其算法複雜度:
平均時間複雜度 | 最好狀況 | 最壞狀況 | 空間複雜度 |
---|---|---|---|
O(nlog₂n) | O(nlog₂n) | O(n²) | O(nlog₂n) |
快速排序排序效率很是高. 雖然它運行最糟糕時將達到O(n²)的時間複雜度, 但一般, 平均來看, 它的時間複雜爲O(nlogn), 比一樣爲O(nlogn)時間複雜度的歸併排序還要快. 快速排序彷佛更偏心亂序的數列, 越是亂序的數列, 它相比其餘排序而言, 相對效率更高. 以前在 捋一捋JS的數組 一文中就提到: Chrome的v8引擎爲了高效排序, 在排序數據超過了10條時, 便會採用快速排序. 對於10條及如下的數據採用的即是插入排序.
Tips: 同選擇排序類似, 快速排序每次交換的元素都有可能不是相鄰的, 所以它有可能打破原來值爲相同的元素之間的順序. 所以, 快速排序並不穩定.
1991年的計算機先驅獎得到者、斯坦福大學計算機科學系教授羅伯特·弗洛伊德(Robert W.Floyd) 和威廉姆斯(J.Williams) 在1964年共同發明了著名的堆排序算法(Heap Sort).
堆排序是利用堆這種數據結構所設計的一種排序算法. 它是選擇排序的一種. 堆分爲大根堆和小根堆. 大根堆要求每一個子節點的值都不大於其父節點的值, 即array[childIndex] <= array[parentIndex], 最大的值必定在堆頂. 小根堆與之相反, 即每一個子節點的值都不小於其父節點的值, 最小的值必定在堆頂. 所以咱們可以使用大根堆進行升序排序, 使用小根堆進行降序排序.
並不是全部的序列都是堆, 對於序列k1, k2,…kn, 須要知足以下條件才行:
ki <= k(2i) 且 ki<=k(2i+1)(1≤i≤ n/2), 即爲小根堆, 將<=換成>=, 那麼則是大根堆. 咱們能夠將這裏的堆看做徹底二叉樹, k(i) 至關因而二叉樹的非葉子節點, k(2i) 則是左子節點, k(2i+1)是右子節點.
算法的基本思想(以大根堆爲例):
先將初始序列K[1..n]建成一個大根堆, 此堆爲初始的無序區.
再將關鍵字最大的記錄K[1] (即堆頂)和無序區的最後一個記錄K[n]交換, 由此獲得新的無序區K[1..n-1]和有序區K[n], 且知足K[1..n-1].keys≤K[n].key
交換K[1] 和 K[n] 後, 堆頂可能違反堆性質, 所以需將K[1..n-1]調整爲堆. 而後重複步驟2, 直到無序區只有一個元素時中止.
以下是動圖效果:
以下是算法實現:
function heapAdjust(array, i, length) {//堆調整 var left = 2 * i + 1, right = 2 * i + 2, largest = i; if (left < length && array[largest] < array[left]) { largest = left; } if (right < length && array[largest] < array[right]) { largest = right; } if (largest != i) { swap(i, largest, array); heapAdjust(array, largest, length); } } function heapSort(array) { //創建大頂堆 length = array.length; for (var i = length>>1; i >= 0; i--) { heapAdjust(array, i, length); } //調換第一個與最後一個元素,從新調整爲大頂堆 for (var i = length - 1; i > 0; i--) { swap(0, i, array); heapAdjust(array, 0, --length); } return array; }
以上, ①創建堆的過程, 從length/2 一直處理到0, 時間複雜度爲O(n);
②調整堆的過程是沿着堆的父子節點進行調整, 執行次數爲堆的深度, 時間複雜度爲O(lgn);
③堆排序的過程由n次第②步完成, 時間複雜度爲O(nlgn).
Tips: 因爲堆排序中初始化堆的過程比較次數較多, 所以它不太適用於小序列. 同時因爲屢次任意下標相互交換位置, 相同元素之間本來相對的順序被破壞了, 所以, 它是不穩定的排序.
計數排序幾乎是惟一一個不基於比較的排序算法, 該算法於1954年由 Harold H. Seward 提出. 使用它處理必定範圍內的整數排序時, 時間複雜度爲O(n+k), 其中k是整數的範圍, 它幾乎比任何基於比較的排序算法都要快( 只有當O(k)>O(n*log(n))的時候其效率反而不如基於比較的排序, 如歸併排序和堆排序).
使用計數排序須要知足以下條件:
待排序的序列所有爲整數
排序須要額外的存儲空間
算法的基本思想:
計數排序利用了一個特性, 對於數組的某個元素, 一旦知道了有多少個其它元素比它小(假設爲m個), 那麼就能夠肯定出該元素的正確位置(第m+1位)
初始化遊標i爲0, 並準備一個緩存數組B, 長度爲待排序數組A的最大值+1, 循環一遍待排序數組A, 在緩存數組B中存儲A的各個元素出現的次數.
①將B中的當前元素item與0比較, 若大於0, 則往待排序數組A中寫入一項A[i] = item; 而後i++, item—; 而後重複步驟①, 直到item==0, 則進入到B的下一個元素中.
遍歷緩存數組B, 即循環執行步驟2. 最終全部有效元素都將依次寫回待排序數組A的第1,2,...n項.
以下是動圖效果:
以下是算法實現:
function countSort(array, max) { var tempLength = max + 1, temp = new Array(tempLength), index = 0, length = array.length; //初始化緩存數組各項的值 for (var i = 0; i < length; i++) { if (!temp[array[i]]) { temp[array[i]] = 0; } temp[array[i]]++; } //依次取出緩存數組的值,並寫入原數組 for (var j = 0; j < tempLength; j++) { while(temp[j] > 0) { array[index++] = j; temp[j]--; } } return array; }
Tips: 計數排序不改變相同元素之間本來相對的順序, 所以它是穩定的排序算法.
桶排序即所謂的箱排序, 它是將數組分配到有限數量的桶子裏. 每一個桶裏再各自排序(所以有可能使用別的排序算法或以遞歸方式繼續桶排序). 當每一個桶裏的元素個數趨於一致時, 桶排序只需花費O(n)的時間. 桶排序經過空間換時間的方式提升了效率, 所以它須要額外的存儲空間(即桶的空間).
算法的基本思想:
桶排序的核心就在於怎麼把元素平均分配到每一個桶裏, 合理的分配將大大提升排序的效率.
以下是算法實現:
function bucketSort(array, bucketSize) { if (array.length === 0) { return array; } var i = 1, min = array[0], max = min; while (i++ < array.length) { if (array[i] < min) { min = array[i]; //輸入數據的最小值 } else if (array[i] > max) { max = array[i]; //輸入數據的最大值 } } //桶的初始化 bucketSize = bucketSize || 5; //設置桶的默認大小爲5 var bucketCount = ~~((max - min) / bucketSize) + 1, //桶的個數 buckets = new Array(bucketCount); //建立桶 for (i = 0; i < buckets.length; i++) { buckets[i] = []; //初始化桶 } //將數據分配到各個桶中,這裏直接按照數據值的分佈來分配,必定範圍內均勻分佈的數據效率最爲高效 for (i = 0; i < array.length; i++) { buckets[~~((array[i] - min) / bucketSize)].push(array[i]); } array.length = 0; for (i = 0; i < buckets.length; i++) { quickSort(buckets[i]); //對每一個桶進行排序,這裏使用了快速排序 for (var j = 0; j < buckets[i].length; j++) { array.push(buckets[i][j]); //將已排序的數據寫回數組中 } } return array; }
Tips: 桶排序自己是穩定的排序, 所以它的穩定性與桶內排序的穩定性保持一致.
實際上, 桶也只是一個抽象的概念, 它的思想與歸併排序,快速排序等相似, 都是經過將大量數據分配到N個不一樣的容器中, 分別排序, 最後再合併數據. 這種方式大大減小了排序時總體的遍歷次數, 提升了算法效率.
基數排序源於老式穿孔機, 排序器每次只能看到一個列. 它是基於元素值的每一個位上的字符來排序的. 對於數字而言就是分別基於個位, 十位, 百位 或千位等等數字來排序. (不明白沒關係, 我也不懂, 請接着往下讀)
按照優先從高位或低位來排序有兩種實現方案:
MSD: 由高位爲基底, 先按k1排序分組, 同一組中記錄, 關鍵碼k1相等, 再對各組按k2排序分紅子組, 以後, 對後面的關鍵碼繼續這樣的排序分組, 直到按最次位關鍵碼kd對各子組排序後. 再將各組鏈接起來, 便獲得一個有序序列. MSD方式適用於位數多的序列.
LSD: 由低位爲基底, 先從kd開始排序,再對kd-1進行排序,依次重複,直到對k1排序後便獲得一個有序序列. LSD方式適用於位數少的序列.
以下是LSD的動圖效果:
)
以下是算法實現:
function radixSort(array, max) { var buckets = [], unit = 10, base = 1; for (var i = 0; i < max; i++, base *= 10, unit *= 10) { for(var j = 0; j < array.length; j++) { var index = ~~((array[j] % unit) / base);//依次過濾出個位,十位等等數字 if(buckets[index] == null) { buckets[index] = []; //初始化桶 } buckets[index].push(array[j]);//往不一樣桶裏添加數據 } var pos = 0, value; for(var j = 0, length = buckets.length; j < length; j++) { if(buckets[j] != null) { while ((value = buckets[j].shift()) != null) { array[pos++] = value; //將不一樣桶裏數據挨個撈出來,爲下一輪高位排序作準備,因爲靠近桶底的元素排名靠前,所以從桶底先撈 } } } } return array; }
以上算法, 若是用來比較時間, 先按日排序, 再按月排序, 最後按年排序, 僅需排序三次.
基數排序更適合用於對時間, 字符串等這些總體權值未知的數據進行排序.
Tips: 基數排序不改變相同元素之間的相對順序, 所以它是穩定的排序算法.
各類排序性能對好比下:
排序類型 | 平均狀況 | 最好狀況 | 最壞狀況 | 輔助空間 | 穩定性 |
---|---|---|---|---|---|
冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | 穩定 |
選擇排序 | O(n²) | O(n²) | O(n²) | O(1) | 不穩定 |
直接插入排序 | O(n²) | O(n) | O(n²) | O(1) | 穩定 |
折半插入排序 | O(n²) | O(n) | O(n²) | O(1) | 穩定 |
希爾排序 | O(n^1.3) | O(nlogn) | O(n²) | O(1) | 不穩定 |
歸併排序 | O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(n) | 穩定 |
快速排序 | O(nlog₂n) | O(nlog₂n) | O(n²) | O(nlog₂n) | 不穩定 |
堆排序 | O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(1) | 不穩定 |
計數排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 穩定 |
桶排序 | O(n+k) | O(n+k) | O(n²) | O(n+k) | (不)穩定 |
基數排序 | O(d(n+k)) | O(d(n+k)) | O(d(n+kd)) | O(n+kd) | 穩定 |
注: 桶排序的穩定性取決於桶內排序的穩定性, 所以其穩定性不肯定. 基數排序中, k表明關鍵字的基數, d表明長度, n表明關鍵字的個數.
願以此文懷念下我那遠去的算法課程.
未完待續...
感謝 http://visualgo.net/ 提供圖片支持. 特別感謝 不是小羊的肖恩 在簡書上發佈的 JS家的排序算法 提供的講解.
本問就討論這麼多內容,你們有什麼問題或好的想法歡迎在下方參與留言和評論.
本文做者: louis
本文連接: http://louiszhai.github.io/20...
參考文章