用Java寫算法之八:桶排序

前面的排序都是比較常規的排序,但下面這個排序也許就不是那麼常規了,它就是桶排序。
java


算法概述/思路算法

桶排序的思想近乎完全的分治思想。假設如今須要對一億個數進行排序。咱們能夠將其等長地分到10000個虛擬的「桶」裏面,這樣,平均每一個桶只有10000個數。若是每一個桶都有序了,則只須要依次輸出爲有序序列便可。具體思路是這樣的:數組

1.將待排數據按一個映射函數f(x)分爲連續的若干段。理論上最佳的分段方法應該使數據平均分佈;實際上,一般採用的方法都作不到這一點。顯然,對於一個已知輸入範圍在【0,10000】的數組,最簡單的分段方法莫過於x/m這種方法,例如,f(x)=x/100。ide

「連續的」這個條件很是重要,它是後面數據按順序輸出的理論保證。函數

2.分配足夠的桶,按照f(x)從數組起始處向後掃描,並把數據放到合適的桶中。對於上面的例子,若是數據有10000個,則咱們須要分配101個桶(由於要考慮邊界條件:f(x)=x/100會產生【0,100】共101種狀況),理想狀況下,每一個桶有大約100個數據。性能

3.對每一個桶進行內部排序,例如,使用快速排序。注意,若是數據足夠大,這裏能夠繼續遞歸使用桶排序,直到數據大小降到合適的範圍。
spa

4.按順序從每一個桶輸出數據。例如,1號桶【112,123,145,189】,2號桶【234,235,250,250】,3號桶【361】,則輸出序列爲【112,123,145,189,234,235,250,250,361】。blog

5.排序完成。排序


代碼實現遞歸

public static void bucketSort(int[] arr){
    //分桶,這裏採用映射函數f(x)=x/10。
    //輸入數據爲0~99之間的數字
    int bucketCount =10;
    Integer[][] bucket = new Integer[bucketCount][arr.length];  //Integer初始爲null,以與數字0區別。
    for (int i=0; i<arr.length; i++){
        int quotient = arr[i]/10;   //這裏便是使用f(x)
        for (int j=0; j<arr.length; j++){
            if (bucket[quotient][j]==null){
                bucket[quotient][j]=arr[i];
                break;
            }
        }
    }
    //小桶排序
    for (int i=0; i<bucket.length; i++){
            //insertion sort
            for (int j=1; j<bucket[i].length; ++j){
                if(bucket[i][j]==null){
                    break;
                }
                int value = bucket[i][j];
                int position=j;
                while (position>0 && bucket[i][position-1]>value){
                    bucket[i][position] = bucket[i][position-1];
                    position--;
                }
                bucket[i][position] = value;
            }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
    }
    //輸出
    for (int i=0, index=0; i<bucket.length; i++){
        for (int j=0; j<bucket[i].length; j++){
            if (bucket[i][j]!=null){
                arr[index] = bucket[i][j];
                index++;
            }
            else{
                break;
            }
        }
    }
}


實現難點

上面的代碼並不長,可是卻很差寫。我在實現過程當中主要遇到了如下問題:

1.最重要的問題:如何得知每一個小桶須要多大?

顯然,N個數平均分到M個桶,每一個桶的容量應該是N/M,但實際數據不可能這麼平均。解決辦法無非是增長桶的容量。那麼,咱們應該增長到多少?

方案一:設定一個固定比例,例如使用10倍於平均的容量。這在不少時候可以解決問題,但遇到極端數據的時候容易出現問題。

方案二:極端增長空間大小,使得每一個桶固定裝一個數,這須要限制輸入數據不重複。可是,若是輸入數據沒有範圍限制,咱們必須申請Integer.MAX_VALUE字節數據,而這必然會致使內存過大,引起Requested array size exceeds VM limit異常。但若是咱們知道其數據範圍,例如[1,100000],則是能夠接受的方案。而且這樣能夠省去排序的步驟,能夠達到線性複雜度,效率很高。

方案三:也就是示例中的代碼,實際上性能並很差。它是把每一個小桶都作到和原始數組同樣大,以犧牲不少空間來換取算法在極限狀況下的健壯性。

2.如何克服Java數組的初始值?

若是是數值型數組,在分桶的時候容易因爲建立數組時系統賦予的0值而給排序形成混亂,干擾結果。這裏有兩種狀況:

A:若是輸入數據明確不爲零,則所受影響不大。只須要在輸出和排序時注意判斷,排除0值就好了。

B:若是數據可能爲零,例如上述代碼,這裏的解決辦法是申請Integer數組。因爲系統初始值爲null,咱們能夠更明確地繞開0值。


算法性能/複雜度

桶排序的時間複雜度能夠從每一步分開分析。

1.分桶的過程,遍歷每一個元素、計算f(x),將x放到桶中,共3n次計算,顯然是O(n)複雜度;

2.最後輸出也是O(n)複雜度;

3.關鍵是小桶內排序的過程:即便使用先進的比較排序算法,也不可繞開O(n㏒n)的下限。所以,每一個小桶的內部複雜度爲n(k㏒k),總得複雜度爲∑(ki*㏒ki)[i=1...m],其中m爲桶的個數,ki爲每一個桶的元素數。儘可能減小桶內數據的數量是提升效率的惟一辦法(由於基於比較排序的最好平均時間複雜度只能達到O(N*logN)了)。所以,有兩種方法:

1)使用更爲平均的劃分,使得不至於某個小桶的數據極多;

2)使用更多的桶,以減小每一個桶數據的數量。極限情況下,每一個桶只有一個數據,這樣就徹底沒有比較操做。可是,在數據極多的狀況下,這樣是很是不現實的,會形成嚴重的空間消耗。這時候就須要權衡時空間複雜度了。

總結起來,設數據共有N個,桶有M個,則桶排序平均複雜度爲:

O(N)+O(N)+O((N/M)*㏒(N/M))=O(N+N*(logN-logM))=O(N+N*logN-N*logM)

最優情形下,桶排序的時間複雜度爲O(n)

桶排序的空間複雜度一般是比較高的,額外開銷爲O(N+M)(由於要維護M個數組的引用)。


算法穩定性

能夠看出,在分桶和從桶依次輸出的過程是穩定的。可是,因爲咱們在第3步使用了其餘算法,因此,桶排序的穩定性依賴於這一步。若是咱們使用了快排,顯然,算法是不穩定的。


算法適用場景

桶排序在數據量很是大,而空間相對充裕的時候是很實用的,能夠大大下降算法的運算數量級。此外,在解決一些特殊問題的時候,桶排序可能會起到意想不到的結果。參考資料中列出了一種。


參考資料

1.桶排序 http://hxraid.iteye.com/blog/647759

相關文章
相關標籤/搜索