數據結構與算法學習十三:基數排序,以空間換時間的穩定式排序,速度很快。

前言

基數排序,屬於桶排序的一種,是一種典型的空間換取時間的 穩定式排序。

一、基數排序(桶排序)介紹

  1. 基數排序(radix sort)屬於 “分配式排序”(distribution sort),又稱“桶子法”(bucket sort)或bin sort,顧名思義,它是通過鍵值的各個位的值,將要排序的元素分配至某些“桶”中,達到排序的作用

  2. 基數排序法是屬於 穩定性的排序,基數排序法的是效率高的穩定性排序法

  3. 基數排序(Radix Sort)是 桶排序的擴展

  4. 基數排序是1887年赫爾曼·何樂禮發明的。它是這樣實現的:將整數按位數切割成不同的數字,然後按每個位數分別比較。

二、基數排序基本思想

  1. 將所有待比較數值統一爲同樣的數位長度,數位較短的數前面補零。
  2. 然後,從最低位開始,依次進行一次排序。
  3. 這樣從最低位排序一直到最高位排序完成以後, 數列就變成一個有序序列。
  4. 這樣說明,比較難理解,下面我們看一個圖文解釋,理解基數排序的步驟

三、圖文解釋

將數組 {53, 3, 542, 748, 14, 214} 使用基數排序, 進行升序排序。
數組的初始狀態 array = {53, 3, 542, 748, 14, 214}

3.1 第 1 次排序

  • 第1輪排序規則:
  1. 每個元素的個位數取出,然後看這個數應該放在哪個對應的桶(一個一維數組)
  2. 按照這個桶的順序(一維數組的下標依次取出數據,放入原來數組)
  • 排序結果:
    數組的第1輪排序結果 array = {542, 53, 3, 14, 214, 748}

在這裏插入圖片描述

3.2 第 2 次排序

  • 第2輪排序規則:
    (1) 將每個元素的十位數取出,然後看這個數應該放在哪個對應的桶(一個一維數組)
    (2) 按照這個桶的順序(一維數組的下標依次取出數據,放入原來數組)

  • 排序結果:
    數組的第2輪排序結果 array = {3, 14, 214, 542, 748, 53}
    在這裏插入圖片描述

3.3 第 3 次排序

  • 第3輪排序規則:
    (1) 將每個元素百位數取出,然後看這個數應該放在哪個對應的桶(一個一維數組)
    (2) 按照這個桶的順序(一維數組的下標依次取出數據,放入原來數組)
  • 排序結果:
    數組的第3輪排序結果 array = {3, 14, 53, 214, 542, 748}
    在這裏插入圖片描述

3.4 結果

到了這裏,是最終的結果。array = {3, 14, 53, 214, 542, 748} ,
所以,排序幾次,需要找到最大數的是幾位數。

四、代碼實現

  1. deductionRadixSort(int[] array); 爲學習推導的過程
  2. radixSort(int[] array);據下面的推導過程,我們得到的最終的基數排序代碼
  3. testTime();測試速度的方法;相當之快,這也正是 空間換取時間的原因所在,
    8萬數據: 134ms, 80萬數據: 269ms , 800萬數據:1s 左右 。
    當爲8000萬數據時,會報錯:java.lang.OutOfMemoryError: Java heap space,內存溢出錯誤,因爲 80000000 * 11(數組) *4(一個int 4個字節) /1024(k) /1024(m)/1024(g) = 3.3G ,會用到 3.3G內存,所以會內存溢出
package com.feng.ch09_sort;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

/*
 * 基數排序(穩定式排序)
 * 是空間換時間的經典算法
 *
 * 速度: 這也正是 空間換取時間的原因所在
 *      相當之快:8萬數據: 134ms, 80萬數據: 269ms , 800萬數據:1s 左右 ,
 *      當爲8000萬數據時,會報錯:java.lang.OutOfMemoryError: Java heap space,內存溢出錯誤,
 *      因爲 80000000 * 11(數組) *4(一個int 4個字節) /1024(k) /1024(m)/1024(g) = 3.3G ,會用到 3.3G內存,所以會內存溢出
 * */
public class S7_RadixSort {

    public static void main(String[] args) {
        int array[] = {53, 3, 542, 748, 14, 214};
        System.out.println("初始數組:");
        System.out.println(Arrays.toString(array));

//        deductionRadixSort(array); // 測試推導方法
        radixSort(array);       // 測試推導後 合成的方法

        System.out.println();
        System.out.println("排序後的數組:");
        System.out.println(Arrays.toString(array));

        // 測試 80000 個數據排序 所用的時間
        System.out.println();
        System.out.println("測試 8000000 個數據 採用基數排序 所用的時間:");
        testTime();   // 8萬數據: 134ms, 80萬數據: 269ms , 800萬數據:1s 左右 , 8000萬數據:
    }

    /*
     * 測試一下 歸併排序的速度, 給 80000 個數據,測試一下
     * */
    public static void testTime() {
        // 創建一個 80000個的隨機的數組
        int array2[] = new int[8000000];
        for (int i = 0; i < 8000000; i++) {
            array2[i] = (int) (Math.random() * 8000000); // 生成一個[ 0, 8000000] 數
        }
//        System.out.println(Arrays.toString(array2)); // 不在打印,耗費時間太長


        long start = System.currentTimeMillis();  //返回以毫秒爲單位的當前時間
        System.out.println("long start:" + start);
        Date date = new Date(start); // 上面的也可以不要,但是我想測試
        System.out.println("date:" + date);
        SimpleDateFormat format = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
        System.out.println("排序前的時間是=" + format.format(date));

        radixSort(array2);

        System.out.println();
        long end = System.currentTimeMillis();
        Date date2 = new Date(end); // 上面的也可以不要,但是我想測試
        System.out.println("排序後的時間是=" + format.format(date2));
        System.out.println("共耗時" + (end - start) + "毫秒");
        System.out.println("毫秒轉成秒爲:" + ((end - start) / 1000) + "秒");
    }

    /*
     * 根據下面的推導過程,我們可以得到最終的基數排序代碼
     * */
    public static void radixSort(int[] array) {

        //1、得到數組中最大的數的位數
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max) {
                max = array[i];
            }
        }
        //2、得到最大數是幾位數
        int maxLength = (max + "").length();
        /*
         * 定義一個二維數組,表示 10 個桶,每個桶就是一個一維數組
         * 說明
         * 1、二維數組包含 10 個一維數組
         * 2、爲了防止在放入數的時候,數據溢出,則每個一維數組(桶),大小定位 array.length
         * 3、很明顯,基數排序是使用空間換時間的經典算法
         * */
        int[][] bucket = new int[10][array.length];

        /*
         * 爲了記錄每個桶中,實際存放了多少個數據,我們定義一個一維數組來記錄各個桶的每次放入的數據的個數
         * 可以這樣理解:
         * 比如:bucketElementCounts[0] , 記錄的就是 bucket[0] 桶的放入數據個數
         * */
        int[] bucketElementCounts = new int[10];

        // 這裏使用循環將代碼處理
        for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
            /*
             * 每一輪(針對每個元素的 對應位 進行排序處理)第一次是個位,第二次是十位,第三次是百位,第四次是千位。。。。
             *      爲了解決這個問題,在上面的 for 循環中,添加了一個 變量 n ,每次 * 10,因爲下面需要取出相應的 個位、十位、百位。。。
             * 注意點:
             *  每輪處理後,需要將每個 bucketElementCounts[i] = 0 !!!
             * */
            for (int j = 0; j < array.length; j++) {
                // 取出每個元素的 個位 的值
                int digitOfElement = (array[j] / n) % 10; // =3,第三個桶   //
                // 放入到對應的桶中
                bucket[digitOfElement][bucketElementCounts[digitOfElement]] = array[j];  // 我覺得第二個 值 有問題
                bucketElementCounts[digitOfElement]++;
            }
            //按照這個桶的順序(一維數組的下標依次取出數據,放入原來數組)
            int index = 0;
            // 遍歷每一個桶,並將桶中的每一個數據,放入到原數組
            for (int k = 0; k < bucket.length; k++) {// 或者爲:k < bucketElementCounts.length  bucket.length 都爲 10
                // 如果桶中有數據,我們才放到原數據
                if (bucketElementCounts[k] != 0) { // bucket[i] !=0
                    // 循環該桶,即第 k 個桶(即第K 個數組中),放入  ;bucketElementCounts[k]: 爲桶的幾個數據
                    for (int j = 0; j < bucketElementCounts[k]; j++) { // bucket[i].length 是桶的長度,但是這裏應該爲 桶裏數據的個數
                        // 取出元素放入到 array
                        array[index] = bucket[k][j];
                        index++;
                    }
                }
                // 第 i+1 輪處理後,需要將每個 bucketElementCounts[i] = 0 !!!
                bucketElementCounts[k] = 0;
            }
//            System.out.println("第 " + (i + 1) + " 輪,對" + n + "位數的排序處理 array = " + Arrays.toString(array)); //
        }
    }

    /*
     * 基數排序
     * 使用逐步推導的方式
     * */
    public static void deductionRadixSort(int[] array) {

        /*
         * 定義一個二維數組,表示 10 個桶,每個桶就是一個一維數組
         * 說明
         * 1、二維數組包含 10 個一維數組
         * 2、爲了防止在放入數的時候,數據溢出,則每個一維數組(桶),大小定位 array.length
         * 3、很明顯,基數排序是使用空間換時間的經典算法
         * */
        int[][] bucket = new int[10][array.length];

        /*
         * 爲了記錄每個桶中,實際存放了多少個數據,我們定義一個一維數組來記錄各個桶的每次放入的數據的個數
         * 可以這樣理解:
         * 比如:bucketElementCounts[0] , 記錄的就是 bucket[0] 桶的放入數據個數
         * */
        int[] bucketElementCounts = new int[10];

        /*
         * 第 1 輪(針對每個元素的個位進行排序處理)
         * 注意點:
         *  第 1 輪處理後,需要將每個 bucketElementCounts[i] = 0 !!!
         * */
        for (int j = 0; j < array.length; j++) {
            // 取出每個元素的 個位 的值
            int digitOfElement = (array[j] / 1) % 10; // =3,第三個桶   //
            // 放入到對應的桶中
            bucket[digitOfElement][bucketElementCounts[digitOfElement]] = array[j];  // 我覺得第二個 值 有問題
            bucketElementCounts[digitOfElement]++;
        }
        //按照這個桶的順序(一維數組的下標依次取出數據,放入原來數組)
        int index = 0;
        // 遍歷每一個桶,並將桶中的每一個數據,放入到原數組
        for (int k = 0; k < bucket.length; k++) {// 或者爲:k < bucketElementCounts.length  bucket.length 都爲 10
            // 如果桶中有數據,我們才放到原數據
            if (bucketElementCounts[k] != 0) { // bucket[i] !=0
                // 循環該桶,即第 k 個桶(即第K 個數組中),放入  ;bucketElementCounts[k]: 爲桶的幾個數據
                for (int j = 0; j < bucketElementCounts[k]; j++) { // bucket[i].length 是桶的長度,但是這裏應該爲 桶裏數據的個數
                    // 取出元素放入到 array
                    array[index] = bucket[k][j];
                    index++;
                }
            }
            // 第 1 輪處理後,需要將每個 bucketElementCounts[i] = 0 !!!
            bucketElementCounts[k] = 0;
        }
        System.out.println("第 1 輪,對個位數的排序處理 array = " + Arrays.toString(array)); //[542, 53, 3, 14, 214, 748]

        /*
         * 第 2 輪(針對每個元素的個位進行排序處理)
         * 注意點:
         *      取出每個元素的 十位 的值:int digitOfElement = (array[j]/10) % 10
         * */
        for (int j = 0; j < array.length; j++) {
            // 取出每個元素的 十位 的值
            int digitOfElement = (array[j] / 10) % 10; // =3,第三個桶   //
            // 放入到對應的桶中
            bucket[digitOfElement][bucketElementCounts[digitOfElement]] = array[j];  // 我覺得第二個 值 有問題
            bucketElementCounts[digitOfElement]++;
        }
        //按照這個桶的順序(一維數組的下標依次取出數據,放入原來數組)
        index = 0;
        // 遍歷每一個桶,並將桶中的每一個數據,放入到原數組
        for (int k = 0; k < bucket.length; k++) {// 或者爲:k < bucketElementCounts.length  bucket.length 都爲 10
            // 如果桶中有數據,我們才放到原數據
            if (bucketElementCounts[k] != 0) { // bucket[i] !=0
                // 循環該桶,即第 k 個桶(即第K 個數組中),放入  ;bucketElementCounts[k]: 爲桶的幾個數據
                for (int j = 0; j < bucketElementCounts[k]; j++) { // bucket[i].length 是桶的長度,但是這裏應該爲 桶裏數據的個數
                    // 取出元素放入到 array
                    array[index] = bucket[k][j];
                    index++;
                }
            }
            // 第 2 輪處理後,需要將每個 bucketElementCounts[i] = 0 !!!
            bucketElementCounts[k] = 0;
        }
        System.out.println("第 2 輪,對十位數的排序處理 array = " + Arrays.toString(array)); // [3, 14, 214, 542, 748, 53]


        /*
         * 第 3 輪(針對每個元素的個位進行排序處理)
         * 注意點:
         *      取出每個元素的 百位 的值:int digitOfElement = (array[j]/100) % 10
         * */
        for (int j = 0; j < array.length; j++) {
            // 取出每個元素的 百位 的值
            int digitOfElement = (array[j] / 100) % 10; // =3,第三個桶   //
            // 放入到對應的桶中
            bucket[digitOfElement][bucketElementCounts[digitOfElement]] = array[j];  // 我覺得第二個 值 有問題
            bucketElementCounts[digitOfElement]++;
        }
        //按照這個桶的順序(一維數組的下標依次取出數據,放入原來數組)
        index = 0;
        // 遍歷每一個桶,並將桶中的每一個數據,放入到原數組
        for (int k = 0; k < bucket.length; k++) {// 或者爲:k < bucketElementCounts.length  bucket.length 都爲 10
            // 如果桶中有數據,我們才放到原數據
            if (bucketElementCounts[k] != 0) { // bucket[i] !=0
                // 循環該桶,即第 k 個桶(即第K 個數組中),放入  ;bucketElementCounts[k]: 爲桶的幾個數據
                for (int j = 0; j < bucketElementCounts[k]; j++) { // bucket[i].length 是桶的長度,但是這裏應該爲 桶裏數據的個數
                    // 取出元素放入到 array
                    array[index] = bucket[k][j];
                    index++;
                }
            }
            // 第 3 輪處理後,需要將每個 bucketElementCounts[i] = 0 !!!
            bucketElementCounts[k] = 0;
        }
        System.out.println("第 3 輪,對百位數的排序處理 array = " + Arrays.toString(array)); // [3, 14, 214, 542, 748, 53]
    }
}

五、基數排序說明

  1. 基數排序是對傳統桶排序的擴展,速度很快.
  2. 基數排序是經典的空間換時間的方式,佔用內存很大, 當對海量數據排序時,容易造成 OutOfMemoryError 。
  3. 基數排序時穩定的。[注:假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序後的序列中,r[i]仍在r[j]之前,則稱這種排序算法是穩定的;否則稱爲不穩定的]
  4. 有負數的數組,我們不用基數排序來進行排序, 如果要支持負數,參考: https://code.i-harness.com/zh-CN/q/e98fa9
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章