本篇有7k+字, 系統梳理了js中排序演算法相關的知識, 希望您能喜歡.
原文: louiszhai.github.io/2016/12/23/…
導讀
排序演算法可以稱得上是我的盲點, 曾幾何時當我知道Chrome的Array.prototype.sort使用了快速排序時, 我的內心是奔潰的(啥是快排, 我只知道冒泡啊?!), 要知道學習一門技術最好的時間是三年前, 但願我現在補習還來得及(捂臉).
因此本篇重拾了出鏡概率比較高的十來種排序演算法, 逐一分析其排序思想, 並批註注意事項. 歡迎對演算法提出改進和討論.
氣泡排序
氣泡排序需要兩個巢狀的迴圈. 其中, 外層迴圈
移動遊標; 內層迴圈
遍歷遊標及之後(或之前)的元素, 通過兩兩交換的方式, 每次只確保該內迴圈結束位置排序正確, 然後內層迴圈
週期結束, 交由外層迴圈
往後(或前)移動遊標, 隨即開始下一輪內層迴圈
, 以此類推, 直至迴圈結束.
Tips: 由於氣泡排序只在相鄰元素大小不符合要求時才調換他們的位置, 它並不改變相同元素之間的相對順序, 因此它是穩定的排序演算法.
由於有兩層迴圈, 因此可以有四種實現方式.
方案 | 外層迴圈 | 內層迴圈 |
---|---|---|
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]建成一個大根堆, 此堆為初始的無序區.
- 再將關鍵字最大的記錄K1 (即堆頂)和無序區的最後一個記錄K[n]交換, 由此得到新的無序區K[1..n-1]和有序區K[n], 且滿足K[1..n-1].keys≤K[n].key
- 交換K1 和 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位)
- 獲取待排序陣列A的最大值, 最小值.
- 將最大值與最小值的差值+1作為長度新建計數陣列B,並將相同元素的數量作為值存入計數陣列.
- 對計數陣列B累加計數, 儲存不同值的初始下標.
- 從原陣列A挨個取值, 賦值給一個新的陣列C相應的下標, 最終返回陣列C.
注意: 如果原陣列A是包含若干個物件的陣列,需要基於物件的某個屬性進行排序,那麼演算法開始時,需要將原陣列A處理為一個只包含物件屬性值的簡單陣列simpleA, 接下來便基於simpleA進行計數、累加計數, 其它同上.
如下是動圖效果:
如下是演算法實現:
function countSort(array, keyName){
var length = array.length,
output = new Array(length),
max,
min,
simpleArray = keyName ? array.map(function(v){
return v[keyName];
}) : array; // 如果keyName是存在的,那麼就建立一個只有keyValue的簡單陣列
// 獲取最大最小值
max = min = simpleArray[0];
simpleArray.forEach(function(v){
v > max && (max = v);
v < min && (min = v);
});
// 獲取計數陣列的長度
var k = max - min + 1;
// 新建並初始化計數陣列
var countArray = new Array(k);
simpleArray.forEach(function(v){
countArray[v - min]= (countArray[v - min] || 0) + 1;
});
// 累加計數,儲存不同值的初始下標
countArray.reduce(function(prev, current, i, arr){
arr[i] = prev;
return prev + current;
}, 0);
// 從原陣列挨個取值(因取的是原陣列的相應值,只能通過遍歷原陣列來實現)
simpleArray.forEach(function(v, i){
var j = countArray[v - min]++;
output[j] = array[i];
});
return output;
}複製程式碼
以上實現不僅支援了數值序列的排序,還支援根據物件的某個屬性值來排序。測試如下:
var a = [2, 1, 1, 3, 2, 1, 4, 2],
b = [
{id: 2, s:'a'},
{id: 1, s: 'b'},
{id: 1, s: 'c'},
{id: 3, s: 'd'},
{id: 2, s: 'e'},
{id: 1, s: 'f'},
{id: 4, s: 'g'},
{id: 2, s: 'h'}
];
countSort(a); // [1, 1, 1, 2, 2, 2, 3, 4]
countSort(b, 'id'); // [{id:1,s:'b'},{id:1,s:'c'},{id:1,s:'f'},{id:2,s:'a'},{id:2,s:'e'},{id:2,s:'h'},{id:3,s:'d'},{id:4,s:'g'}]複製程式碼
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代表關鍵字的個數.
願以此文懷念下我那遠去的演算法課程.
未完待續...
感謝 visualgo.net/ 提供圖片支援. 特別感謝 不是小羊的肖恩 在簡書上釋出的 JS家的排序演算法 提供的講解.
本問就討論這麼多內容,大家有什麼問題或好的想法歡迎在下方參與留言和評論.
本文作者: louis
本文連結: louiszhai.github.io/2016/12/23/…
參考文章