前言
本文為算法分析系列博文之一,深入探究桶排序,分析各自環境下的性能,同時輔以性能分析示例加以佐證
實現思路與步驟
思路
- 設置固定空桶數
- 將數據放到對應的空桶中
- 將每個不為空的桶進行排序
- 拼接不為空的桶中的數據,得到結果
步驟演示
假設一組數據(20長度)為
[63,157,189,51,101,47,141,121,157,156,194,117,98,139,67,133,181,13,28,109]
現在需要按5個分桶,進行桶排序,實現步驟如下:
-
找到數組中的最大值194和最小值13,然后根據桶數為5,計算出每個桶中的數據范圍為
(194-13+1)/5=36.4
-
遍歷原始數據,(以第一個數據63為例)先找到該數據對應的桶序列
Math.floor(63 - 13) / 36.4) =1
,然后將該數據放入序列為1的桶中(從0開始算) -
當向同一個序列的桶中第二次插入數據時,判斷桶中已存在的數字與新插入的數字的大小,按從左到右,從小打大的順序插入。如第一個桶已經有了63,再插入51,67后,桶中的排序為(51,63,67) 一般通過鏈表來存放桶中數據,但js中可以使用數組來模擬
-
全部數據裝桶完畢后,按序列,從小到大合並所有非空的桶(如0,1,2,3,4桶)
-
合並完之后就是已經排完序的數據
步驟圖示
實現代碼
以下分別以JS和Java的實現代碼為例
JS實現代碼(數組替代鏈表版本)
var bucketSort = function(arr, bucketCount) {
if (arr.length <= 1) {
return arr;
}
bucketCount = bucketCount || 10;
//初始化桶
var len = arr.length,
buckets = [],
result = [],
max = arr[0],
min = arr[0];
for (var i = 1; i < len; i++) {
min = min <= arr[i] ? min: arr[i];
max = max >= arr[i] ? max: arr[i];
}
//求出每一個桶的數值范圍
var space = (max - min + 1) / bucketCount;
//將數值裝入桶中
for (var i = 0; i < len; i++) {
//找到相應的桶序列
var index = Math.floor((arr[i] - min) / space);
//判斷是否桶中已經有數值
if (buckets[index]) {
//數組從小到大排列
var bucket = buckets[index];
var k = bucket.length - 1;
while (k >= 0 && buckets[index][k] > arr[i]) {
buckets[index][k + 1] = buckets[index][k];
k--
}
buckets[index][k + 1] = arr[i];
} else {
//新增數值入桶,暫時用數組模擬鏈表
buckets[index] = [];
buckets[index].push(arr[i]);
}
}
//開始合並數組
var n = 0;
while (n < bucketCount) {
if (buckets[n]) {
result = result.concat(buckets[n]);
}
n++;
}
return result;
};
//開始排序
arr = bucketSort(arr, self.bucketCount);
JS實現代碼(模擬鏈表實現版本)
var L = require('linklist'); //鏈表
var sort = function(arr, bucketCount) {
if(arr.length <= 1) {
return arr;
}
bucketCount = bucketCount || 10;
//初始化桶
var len = arr.length,
buckets = [],
result = [],
max = arr[0],
min = arr[0];
for(var i = 1; i < len; i++) {
min = min <= arr[i] ? min : arr[i];
max = max >= arr[i] ? max : arr[i];
}
//求出每一個桶的數值范圍
var space = (max - min + 1) / bucketCount;
//將數值裝入桶中
for(var i = 0; i < len; i++) {
//找到相應的桶序列
var index = Math.floor((arr[i] - min) / space);
//判斷是否桶中已經有數值
if(buckets[index]) {
//數組從小到大排列
var bucket = buckets[index];
var insert = false; //插入標石
L.reTraversal(bucket, function(item, done) {
if(arr[i] <= item.v) { //小於,左邊插入
L.append(item, _val(arr[i]));
insert = true;
done(); //退出遍歷
}
});
if(!insert) { //大於,右邊插入
L.append(bucket, _val(arr[i]));
}
} else {
var bucket = L.init();
L.append(bucket, _val(arr[i]));
buckets[index] = bucket; //鏈表實現
}
}
//開始合並數組
for(var i = 0, j = 0; i < bucketCount; i++) {
L.reTraversal(buckets[i], function(item) {
// console.log(i+":"+item.v);
result[j++] = item.v;
});
}
return result;
};
//鏈表存儲對象
function _val(v) {
return {
v: v
}
}
//開始排序
arr = bucketSort(arr, self.bucketCount);
其中,linklist為引用的第三方庫,地址
linklist
Java實現代碼
public static double[] bucketSort(double arr[], int bucketCount) {
int len = arr.length;
double[] result = new double[len];
double min = arr[0];
double max = arr[0];
//找到最大值和最小值
for (int i = 1; i < len; i++) {
min = min <= arr[i] ? min: arr[i];
max = max >= arr[i] ? max: arr[i];
}
//求出每一個桶的數值范圍
double space = (max - min + 1) / bucketCount;
//先創建好每一個桶的空間,這里使用了泛型數組
ArrayList < Double > [] arrList = new ArrayList[bucketCount];
//把arr中的數均勻的的分布到[0,1)上,每個桶是一個list,存放落在此桶上的元素
for (int i = 0; i < len; i++) {
int index = (int) Math.floor((arr[i] - min) / space);
if (arrList[index] == null) {
//如果鏈表里沒有東西
arrList[index] = new ArrayList < Double > ();
arrList[index].add(arr[i]);
} else {
//排序
int k = arrList[index].size() - 1;
while (k >= 0 && (Double) arrList[index].get(k) > arr[i]) {
if (k + 1 > arrList[index].size() - 1) {
arrList[index].add(arrList[index].get(k));
} else {
arrList[index].set(k + 1, arrList[index].get(k));
}
k--;
}
if (k + 1 > arrList[index].size() - 1) {
arrList[index].add(arr[i]);
} else {
arrList[index].set(k + 1, arr[i]);
}
}
}
//把各個桶的排序結果合並 ,count是當前的數組下標
int count = 0;
for (int i = 0; i < bucketCount; i++) {
if (null != arrList[i] && arrList[i].size() > 0) {
Iterator < Double > iter = arrList[i].iterator();
while (iter.hasNext()) {
Double d = (Double) iter.next();
result[count] = d;
count++;
}
}
}
return result;
}
//開始排序,其中arr為需要排序的數組
double[] result = bucketSort(arr,bucketCount);
算法復雜度
算法復雜度的計算,這里我們直接拋開常數,只計算與N(數組長度)與M(分桶數)相關的語句
時間復雜度
因為時間復雜度度考慮的是最壞的情況,所以桶排序的時間復雜度可以這樣去看(只看主要耗時部分,而且常熟部分K一般都省去)
- N次循環,每一個數據裝入桶
- 然后M次循環,每一個桶中的數據進行排序(每一個桶中有N/M個數據),假設為使用比較先進的排序算法進行排序
一般較為先進的排序算法時間復雜度是O(N*logN),實際的桶排序執行過程中,桶中數據是以鏈表形式插入的,那么整個桶排序的時間復雜度為:
O(N)+O(M*(N/M)*log(N/M))=O(N*(log(N/M)+1))
所以,理論上來說(N個數都符合均勻分布),當M=N時,有一個最小值為O(N)
PS:這里有人提到最后還有M個桶的合並,其實首先M一般遠小於N,其次再效率最高時是M=N,這是就算把這個算進去,也是O(N(1+log(N/M)+M/N)),極小值還是O(2N)=O(N)
求M的極小值,具體計算為:(其中N可以看作一個很大的常數)
F(M) = log(N/M)+M/N) = LogN-LogM+M/N
它的導函數
F'(M) = -1/M + 1/N
因為導函數大於0代表函數遞增,小於0代表函數遞減
所以F(M)在(0,N) 上遞減
在(N,+∞)上遞增
所以當M=N時取到極小值
空間復雜度
空間復雜度一般指算法執行過程中需要的額外存儲空間
桶排序中,需要創建M個桶的額外空間,以及N個元素的額外空間
所以桶排序的空間復雜度為 O(N+M)
穩定性
穩定性是指,比如a在b前面,a=b,排序后,a仍然應該在b前面,這樣就算穩定的。
桶排序中,假如升序排列,a已經在桶中,b插進來是永遠都會a右邊的(因為一般是從右到左,如果不小於當前元素,則插入改元素的右側)
所以桶排序是穩定的
PS:當然了,如果采用元素插入后再分別進行桶內排序,並且桶內排序算法采用快速排序,那么就不是穩定的
適用范圍
用排序主要適用於均勻分布的數字數組,在這種情況下能夠達到最大效率
性能分析
為了更好的測試桶排序在各自環境的性能,分別用普通JS瀏覽器,Node.js環境,Java環境進行測試,得出以下的對比分析
前提數據為:
- 10W長度的隨機數組
- 數組的范圍為[0,10000)
- 數據為浮點類型
JS瀏覽器環境下的性能(數組替代鏈表型)
本文主要是在webkit內核的瀏覽器中測試,瀏覽器中的方案類型為
- 數據插入時排序,但是使用數組替代鏈表
出人意料,答案並非是理想的那樣。
結果為:
- 當分桶數從1-500時,排序效率有所提升(其中[1,100]提升的比較明顯)
- 當分桶數大於500后,再增加分桶數,性能反而會有明顯下降
- 而且,排序時間過長,已經超過了毫秒級別
- 所以,明顯並不符合理想預期
詳細結果
以下為在前提條件下,分桶數從10-10000變化的耗時對比
分桶數 | 耗時 | 趨勢 |
---|---|---|
10 | 24444ms | 遞減 |
100 | 3246ms | 遞減 |
500 | 3104ms | 遞減 |
1000 | 3482ms | 遞增 |
10000 | 9185ms | 遞增 |
圖示
其中,分桶為500時的一個排序結果圖示(其中平均排序時間在2-3S,超過了理想模型下的預期時間)
為了探討是桶排序自身的原因還是JS瀏覽器環境的局限,所以又單獨在Node.js環境下和Java環境下進行分析測試
Node.js環境下的性能(數組替代鏈表型)
這種方案下采用和瀏覽器中一樣的代碼(數組替代鏈表型)
結果為:
- 當分桶數從1-500時,排序效率有所提升(其中[1,100]提升的比較明顯)
- 當分桶數大於500后,再增加分桶數,性能反而會有明顯下降
- 而且,排序時間過長,已經超過了毫秒級別
- 所以,明顯並不符合理想預期模型
詳細結果
以下為在前提條件下,分桶數從1-1000000變化的耗時對比
分桶數 | 耗時 | 趨勢 |
---|---|---|
1 | 9964ms | 遞減 |
10 | 1814ms | 遞減 |
100 | 279ms | 遞減 |
500 | 204ms | 遞減 |
1000 | 262ms | 遞增 |
5000 | 1078ms | 遞增 |
10000 | 2171ms | 遞增 |
100000 | 9110ms | 遞增 |
Node.js環境下的性能(模擬鏈表型)
這種方案下采用和瀏覽器中一樣的代碼(模擬鏈表型),這種方案里的主要差別是不再使用數組替代鏈表,而是采用模擬鏈表的方式
結果為:
- 整個1-100000區間,隨着分桶數的增加,效率是遞增的
- 當分桶數從1-1000時,性能遠遠小於前面的那種數組替代鏈表類型
- 當分桶數大於1000后,再增加分桶數,性能才逐漸超過前面的那種類型
- 所以,雖然說這種算法在分桶數較低時性能很低,但是當分桶數提高時,性能有着明顯的提供,而且性能和分桶數是線性關系,符合理想預期模型
詳細結果
以下為在前提條件下,分桶數從1-1000000變化的耗時對比
分桶數 | 耗時 | 趨勢 |
---|---|---|
1 | 196405ms | 遞減 |
10 | 30527ms | 遞減 |
100 | 3029ms | 遞減 |
500 | 976ms | 遞減 |
1000 | 643ms | 遞減 |
5000 | 340ms | 遞減 |
10000 | 276ms | 遞減 |
100000 | 312ms | 穩定 |
1000000 | 765ms | 遞增 |
Java環境下的性能
這種方案主要用來和Node.js后台執行方案的對比
結果為:
- 分桶數從小到大增加時,性能逐步增加
- 當分桶數在10000左右時,達到性能最大值
- 分桶數在往后增加也不會影響性能(因為實際上沒有用到計算)
- 雖然說與理想值還有一點差距,但整個結果基本符合預期
詳細結果
以下為在前提條件下,分桶數從1-1000000變化的耗時對比
分桶數 | 耗時 | 趨勢 |
---|---|---|
1 | 39610ms | 遞減 |
10 | 6094ms | 遞減 |
100 | 1127ms | 遞減 |
500 | 361ms | 遞減 |
10000 | 192ms | 遞減 |
100000 | 195ms | 穩定 |
1000000 | 198ms | 穩定 |
總結
桶排序決定快慢的關鍵在於桶內元素的排序算法,所以不同的實現算法,相應的排序代價也是不一樣的
比如,本文中的幾個對比
- 使用數組模擬鏈表,桶內元素插入時即排序
- 使用模擬鏈表,桶內元素插入時即排序
以上幾種的排序方案,最終的結果都是不一樣的。
而且還有一點值得注意,瀏覽器中執行的性能損耗要遠大於后端執行。
關於JS數組替代鏈表方案的性能疑惑
最開始分析桶排序時,只采用了JS數組替代鏈表的方案,那時候發現當分桶數大於一定閾值時,性能會有一個明顯的下降,剛開始還比較疑惑,不知道是桶排序自身的問題還是瀏覽器環境的限制還是算法的問題。
直到后來又分別在Java環境,Node.js環境進行測試,並且嘗試更換算法,最終發現原來有以下原因:
- 瀏覽器中執行的性能損耗要遠大於后端執行
- 使用數組替代鏈表型,這個方案本身有問題
- 另外還試過使用數組替代鏈表,先插入數據,全部插入完畢后再單個桶內進行快速排序,結果表明這種方案的結果與前面的數組替代鏈表型是基本一致的
而且后來采用模擬鏈表方案,發現結果確實是與預期預估的趨勢相符合的。
所以基本鎖定的原因就是:JS中使用數組替代鏈表這種方案本身就不合理
關於如何選擇桶排序方案
上述分析中可以看到,當分桶數較小時,模擬鏈表方案性能要遠遠小於數組替代鏈表方案,但基本上當分桶數大於1000多時,模擬鏈表方案的優勢就體現出來了。
所以實際情況可以根據實際的需要進行選擇
示例Demo
仍然和以前的系列一樣,有提供一個瀏覽器環境下的性能分析示例工具,參考
JS幾種數組排序方式分析比較
原文地址
原文在我個人博客上面
排序算法之桶排序的深入理解以及性能分析