排序算法 | 桶排序算法原理及實現和優化

排序充斥着我們的生活,比如站隊、排隊買票、考試排名、公司業績排名、將電子郵件按時間排序、QQ 好友列表中的會員紅名靠前,等等。

這裏先舉個例子,通過這個例子讓我們接觸第 1 個算法。

在某個期末考試中,老師要把大家的分數排序,比如有 5 個學生,分別考 5、9、5、1、6 分(滿分 10 分),從大到小排序應該是 9、6、5、5、1,大家有沒有辦法寫一段程序隨機讀取 5 個數,然後對它們排序呢?

看到這個問題,我們用 5 分鐘想一下該怎麼辦。辦法當然很多,這裏使用桶排序的思想來處理。

我們找到 11 個桶,分別編號爲 0-10,對應 0-10 分,如圖 1 所示。

圖 1 準備 11 個桶並編號

着我們把這些分數按照桶的編號放入桶中,如圖 2 所示。

圖 1 準備 11 個桶並編號

接着我們從最大編號的桶到最小編號的桶依次輸出每個桶中的分數,分別是 9、6、5、5、1 了。是不是很輕鬆地完成排序了呢?這就是桶排序的思想。

什麼是桶排序


桶排序,也叫作箱排序,是一個排序算法,也是所有排序算法中最快、最簡單的排序算法。其中的思想是我們首先需要知道所有待排序元素的範圍,然後需要有在這個範圍內的同樣數量的桶,接着把元素放到對應的桶中,最後按順序輸出。

這實際上是簡易版的桶排序,我們想象一下,如果考試分數的範圍是 0~100 萬該怎麼辦?弄 100 萬個桶嗎?

實際上在這種情況下,一個桶並不總是放同一個元素,在很多時候一個桶裏可能會放多個元素,這是不是與散列表有點相似呢?其實真正的桶排序和散列表有一樣的原理。

除了對一個桶內的元素做鏈表存儲,我們也有可能對每個桶中的元素繼續使用其他排序算法進行排序,所以更多時候,桶排序會結合其他排序算法一起使用。

桶排序的實現


簡易版實現

我們怎麼在代碼中實現桶排序呢?其實很簡單,使用數組就好了。比如有 11 個桶,我們只需要聲明一個長度爲 11 的數組,然後每把一個元素往桶中放時,就把數組指定位置的值加 1,最終倒序輸出數組的下標,數組每個位置的值爲幾就輸出幾次下標,這樣就可以實現桶排序了。

下面我們一起看看簡易版桶排序的代碼。

public class BucketSort01 {

    public static void main(String[] args) {
        int[] array = {5, 9, 1, 9, 5, 3, 7, 6, 1};// 待排序數組
        int[] buckets = new int[11];
        sort(array, buckets);
        print(buckets);
    }

    /** 從小到大排序 */
    public static void sort(int array[], int buckets[]) {
        for (int i = 0; i < array.length; i++) {
            buckets[array[i]]++;
        }
    }

    /** 從小到大排序 */
    public static void print(int buckets[]) {
        // 順序輸出數據
        for (int i = 0; i < buckets.length; i++) {
            // 元素中值爲幾,說明有多少個相同值的元素,則輸出幾遍
            for (int j = 0; j < buckets[i]; j++) {
                System.out.print(i + "  ");
            }
        }
    }
}
正式版實現

一個桶並不總是放同一個元素,在很多時候一個桶裏可能會放多個元素。

public class BucketSort03 {

    public static void main(String[] args) {
        int[] array = {50, 9, 1, 9, 53, 33, 27, 6, 1};// 待排序數組
        sort(array);
        print(array);
    }

    /** 從小到大排序 */
    public static void sort(int[] array) {

        // 確定元素的最值
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        for (int i = 0; i < array.length; i++) {
            max = Math.max(max, array[i]);
            min = Math.min(min, array[i]);
        }

        // 桶數:(max - min) / array.length的結果爲數組大小的倍數(最大倍數),以倍數作爲桶數
        int bucketNum = (max - min) / array.length + 1;
        // 初始化桶
        ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
        for (int i = 0; i < bucketNum; i++) {
            bucketArr.add(new ArrayList<Integer>());
        }

        // 將每個元素放入桶
        for (int i = 0; i < array.length; i++) {
            // 計算每個(array[i] - min)是數組大小的多少倍,看看放入哪個桶裏
            int num = (array[i] - min) / (array.length);
            bucketArr.get(num).add(array[i]);
        }

        // 對每個桶進行排序
        for (int i = 0; i < bucketArr.size(); i++) {
            Collections.sort(bucketArr.get(i));
        }

        // 合併數據
        int j = 0;
        for (ArrayList<Integer> tempList : bucketArr) {
            for (int i : tempList) {
                array[j++] = i;
            }
        }
    }

    /** 打印數組 */
    public static void print(int array[]) {
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + "   ");
        }
        System.out.println();
    }
}
高級版實現

基於基數排序實現的桶排序。參考:基數排序算法原理及實現和優化

public class BucketSort04 {

    public static void main(String[] args) {
        int[] array = {51, 944, 1, 9, 57, 366, 79, 6, 1, 345};// 待排序數組
        sort(array);

        System.out.println("最終排好序的數據:");
        print(array);
    }

    /**
     * 從小到大排序
     */
    public static void sort(int data[]) {
        int n = data.length;
        // 使用數組來模擬鏈表(當然犧牲了部分的空間,但是操作卻是簡單了很多,穩定性也大大提高了)
        // 十個桶。建立一個二維數組,行向量的下標0—9代表了10個桶,每個行形成的一維數組則是桶的空間
        int bask[][] = new int[10][n];
        // 用來計算每個桶使用的容量
        int index[] = new int[10];
        // 計算最大的數有多少位。比如:5978,有4位
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < n; i++) {
            int k = (data[i] + "").length();
            max = max > k ? max : k;
        }
        String str;
        // 循環內將所有數據補齊,長度都爲 max 。第一輪 i 代表個位,第二輪 i 代表十位。。。
        // 按照 個、十、百、千...的位置來計算
        // 第一輪將10以內的數據排好序,第二輪將100以內的數據排好序......
        for (int i = max - 1; i >= 0; i--) {

            System.out.println("第" + (max - i) + "輪補齊後的數據:");

            // 所有的數字都循環一遍
            for (int j = 0; j < n; j++) {
                str = "";
                // 按照 max 將所有的數據補齊,位數不足的前面補零
                if (Integer.toString(data[j]).length() < max) {
                    for (int k = 0; k < max - Integer.toString(data[j]).length(); k++)
                        str += "0";
                }
                str += Integer.toString(data[j]);

                System.out.printf("%5s", str);

                // index[str.charAt(i) - '0']用於第二層循環計算每個桶使用的容量,第二層循環結束後會將index[str.charAt(i) - '0']都初始化爲零
                // 第一輪取 str 的個位(str.charAt(i--)),放在第(str.charAt(i--) - '0')個桶的第(index[str.charAt(i) - '0']++)個位置
                // 第二輪取 str 的十位(str.charAt(i--)),放在第(str.charAt(i--) - '0')個桶的第(index[str.charAt(i) - '0']++)個位置
                // .......
                bask[str.charAt(i) - '0'][index[str.charAt(i) - '0']++] = data[j];
            }

            // 將桶內的數據重新放入data數組內
            int pos = 0;
            for (int j = 0; j < 10; j++) {
                // 第j個桶內有index[j]個數據
                for (int k = 0; k < index[j]; k++) {
                    data[pos++] = bask[j][k];
                }
            }
            
            System.out.println();
            System.out.println("第" + (max - i) + "輪index內的數據:");
            print(index);

            System.out.println("第" + (max - i) + "輪桶內的數據:");
            print(bask);

            System.out.println("第" + (max - i) + "輪結束後data內的數據:");
            print(data);
            System.out.println();

            // 將index[x]歸零
            for (int x = 0; x < 10; x++) index[x] = 0;
        }
    }

    public static void print(int array[][]) {
        for (int j = 0; j < array.length; j++) {
            for (int k = 0; k < array[j].length; k++) {
                System.out.printf("%5d", array[j][k]);
            }
            System.out.println();
        }
    }

    public static void print(int array[]) {
        for (int j = 0; j < array.length; j++) {
            System.out.printf("%5d", array[j]);
        }
        System.out.println();
    }
}

控制檯輸出:

第一輪將10以內的數據排好序,第二輪將100以內的數據排好序…

1輪補齊後的數據:
  051  944  001  009  057  366  079  006  001  3451輪index內的數據:
    0    3    0    0    1    1    2    1    0    21輪桶內的數據:
    0    0    0    0    0    0    0    0    0    0
   51    1    1    0    0    0    0    0    0    0
    0    0    0    0    0    0    0    0    0    0
    0    0    0    0    0    0    0    0    0    0
  944    0    0    0    0    0    0    0    0    0
  345    0    0    0    0    0    0    0    0    0
  366    6    0    0    0    0    0    0    0    0
   57    0    0    0    0    0    0    0    0    0
    0    0    0    0    0    0    0    0    0    0
    9   79    0    0    0    0    0    0    0    01輪結束後data內的數據:
   51    1    1  944  345  366    6   57    9   792輪補齊後的數據:
  051  001  001  944  345  366  006  057  009  0792輪index內的數據:
    4    0    0    0    2    2    1    1    0    02輪桶內的數據:
    1    1    6    9    0    0    0    0    0    0
   51    1    1    0    0    0    0    0    0    0
    0    0    0    0    0    0    0    0    0    0
    0    0    0    0    0    0    0    0    0    0
  944  345    0    0    0    0    0    0    0    0
   51   57    0    0    0    0    0    0    0    0
  366    6    0    0    0    0    0    0    0    0
   79    0    0    0    0    0    0    0    0    0
    0    0    0    0    0    0    0    0    0    0
    9   79    0    0    0    0    0    0    0    02輪結束後data內的數據:
    1    1    6    9  944  345   51   57  366   793輪補齊後的數據:
  001  001  006  009  944  345  051  057  366  0793輪index內的數據:
    7    0    0    2    0    0    0    0    0    13輪桶內的數據:
    1    1    6    9   51   57   79    0    0    0
   51    1    1    0    0    0    0    0    0    0
    0    0    0    0    0    0    0    0    0    0
  345  366    0    0    0    0    0    0    0    0
  944  345    0    0    0    0    0    0    0    0
   51   57    0    0    0    0    0    0    0    0
  366    6    0    0    0    0    0    0    0    0
   79    0    0    0    0    0    0    0    0    0
    0    0    0    0    0    0    0    0    0    0
  944   79    0    0    0    0    0    0    0    03輪結束後data內的數據:
    1    1    6    9   51   57   79  345  366  944

最終排好序的數據:
    1    1    6    9   51   57   79  345  366  944

桶排序的時間複雜度


對於N個待排數據,M個桶,平均每個桶[N/M]個數據的桶排序平均時間複雜度爲:

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

當N=M時,即極限情況下每個桶只有一個數據時。桶排序的最好效率能夠達到O(N)。

總結:桶排序的平均時間複雜度爲線性的O(N+C),其中C=N*(logN-logM)。如果相對於同樣的N,桶數量M越大,其效率越高,最好的時間複雜度達到O(N)。當然桶排序的空間複雜度爲O(N+M),如果輸入數據非常龐大,而桶的數量也非常多,則空間代價無疑是昂貴的。此外,桶排序是穩定的

通過上面的性能分析,我們可以知道桶排序的特點,那就是速度快、簡單,但是也有相應的弱點,那就是空間利用率低,如果數據跨度過大,則空間可能無法承受,或者說這些元素並不適合使用桶排序算法。

桶排序的適用場景


桶排序的適用場景非常明瞭,那就是在數據分佈相對比較均勻或者數據跨度範圍並不是很大時,排序的速度還是相當快且簡單的。

但是當數據跨度過大時,這個空間消耗就會很大;如果數值的範圍特別大,那麼對空間消耗的代價肯定也是不切實際的,所以這個算法還有一定的侷限性。同樣,由於時間複雜度爲 O(n+m),如果 m 比 n 大太多,則從時間上來說,性能也並不是很好。

但是實際上在使用桶排序的過程中,我們會使用類似散列表的方式去實現,這時的空間利用率會高很多,同時時間複雜度會有一定的提升,但是效率還不錯。

我們在開發過程中,除了對一些要求特別高並且數據分佈較爲均勻的情況使用桶排序,還是很少使用桶排序的,所以即使桶排序很簡單、很快,我們也很少使用它。

桶排序更多地被用於一些特定的環境,比如數據範圍較爲侷限或者有一些特定的要求,比如需要通過哈希映射快速獲取某些值、需要統計每個數的數量。但是這一切都需要確認數據的範圍,如果範圍太大,就需要巧妙地解決這個問題或者使用其他算法了。

桶排序的應用


百度百科:https://baike.baidu.com/item/桶排序/4973777#5

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章