排序算法之桶排序的深入理解以及性能分析


前言

本文為算法分析系列博文之一,深入探究桶排序,分析各自環境下的性能,同時輔以性能分析示例加以佐證

實現思路與步驟

思路

  1. 設置固定空桶數
  2. 將數據放到對應的空桶中
  3. 將每個不為空的桶進行排序
  4. 拼接不為空的桶中的數據,得到結果

步驟演示

假設一組數據(20長度)為

[63,157,189,51,101,47,141,121,157,156,194,117,98,139,67,133,181,13,28,109] 

現在需要按5個分桶,進行桶排序,實現步驟如下:

  1. 找到數組中的最大值194和最小值13,然后根據桶數為5,計算出每個桶中的數據范圍為(194-13+1)/5=36.4

  2. 遍歷原始數據,(以第一個數據63為例)先找到該數據對應的桶序列 Math.floor(63 - 13) / 36.4) =1,然后將該數據放入序列為1的桶中(從0開始算)

  3. 當向同一個序列的桶中第二次插入數據時,判斷桶中已存在的數字與新插入的數字的大小,按從左到右,從小打大的順序插入。如第一個桶已經有了63,再插入51,67后,桶中的排序為(51,63,67) 一般通過鏈表來存放桶中數據,但js中可以使用數組來模擬

  4. 全部數據裝桶完畢后,按序列,從小到大合並所有非空的桶(如0,1,2,3,4桶)

  5. 合並完之后就是已經排完序的數據

步驟圖示

實現代碼

以下分別以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幾種數組排序方式分析比較

原文地址

原文在我個人博客上面
排序算法之桶排序的深入理解以及性能分析

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM