JAVA 數據結構與算法(二)—— 排序算法(冒泡、選擇、插入、希爾、快速、歸併、計數、基數、堆排序和桶排序)

一、JAVA 算法介紹

1、排序算法(Sort Algorithm)

(1)排序的分類
① 內部排序:指將需要處理的所有數據都加在到內部存儲器中進行排序(使用內存)。
② 外部排序:數據量過大,無法全部加載到內存中,需要藉助外部儲存進行排序(使用內存和外存結合)。
在這裏插入圖片描述

(2)算法的時間複雜度
一般情況下,算法中的基本操作語句的重複執行次數是問題規模n的某個函數,用T(n)表示,若有某個輔助函數f(n),使得當n趨近於無窮大時,T(n) / f(n)的極限值爲不等於零的常數,則稱f(n)是T(n)的同數量級函數。 記作T(n)=O(f(n)),稱O(f(n)) 爲算法的漸進時間複雜度,簡稱時間複雜度。T(n)不同,但時間複雜度可能相同。如: T(n)=n2+7n+6 與T(n)=3n2+2n+2它們的T(n)不同,但時間複雜度相同,都爲0(n2)。

度量一個程序(算法)執行時間的兩種方法:事後統計的方法、事前估算的方法。

  • 事後統計的方法:
    這種方法可行,但是有兩個問題:一是要想對設計的算法的運行性能進行評測,需要實際運行該程序,二是所得時間的統計量依賴於計算機的硬件、軟件等環境因素。這種方式,要在同一臺計算機的相同狀態下運行,才能比較那個算法速度更快。
  • 事前估算的方法:
    通過分析某個算法的時間複雜度來判斷哪個算法更優。

常見的時間複雜度

  • 常數階O(1)
  • 對數階O(log2n)
  • 線性階O(n)
  • 線性對數階O(nlog2n)
  • 平方階O(n^2)
  • 立方階O(n^3)
  • k次方階O(n^k)
  • 指數階O(2^n)

常見的算法時間複雜度由小到大依次爲: O(1)<O(log2n)<O(n)<o(nlog2n)<O(n^2)<0(n^3)< O(n^k)<0(2^n),隨着問題規模n的不斷增大,上述時間複雜度不斷增大,算法的執行效率越低

平均時間複雜度和最壞時間複雜度

  • 平均時間複雜度是指所有可能的輸入實例均以等概率出現的情況下,該算法的運行時間。
  • 最壞情況下的時間複雜度稱最壞時間複雜度。一般討論的時間複雜度均是最壞情況下的時間複雜度。這樣做的原因是:最壞情況下的時間複雜度是算法在任何輸入實例上運行時間的界限,這就保證.了算法的運行時間不會比最壞情況更長。
  • 平均時間複雜度和最壞時間複雜度是否一致,和算法有關(如圖)。

在這裏插入圖片描述

(3)空間複雜度

  • 類似於時間複雜度的討論,一個算法的空間複雜度(Space Complexity)定義爲該算法所耗費的存儲空間,它也是問題規模n的函數。
  • 空間複雜度(SpaceComplexity)是對一個算法在運行過程中臨時佔用存儲空間大小的量度。有的算法需要佔用的臨時工作單元數與解決問題的規模n有關,它隨着n的增大而增大,當n較大時,將佔用較多的存儲單元,例如快速排序和歸併排序算法就屬於這種情況。
  • 在做算法分析時,主要討論的是時間複雜度。從用戶使用體驗上看,更看重的程序執行的速度。一些緩 存產品(redis, memcache)和算法(基數排序)本質就是用空間換時間。

(4)時間頻度
時間頻度是指一個算法花費的時間與算法中語句的執行次數成正比例,哪個算法中語句執行次數多,它花費時間就多。一個算法中的語句執行次數稱爲語句頻度或時間頻度,記爲T(n)。

二、常用排序算法示例

1、冒泡排序法

(1)冒泡排序法介紹
冒泡排序法(Bubble Sorting)是通過對待排序序列從前向後(從下標較小的元素開始) , 依次比較相鄰元素的值,若發現逆序則交換,使值較大的元素逐漸從前移向後部,就象水底下的氣泡一樣逐漸向上冒。

(2)圖解冒泡法排序
在這裏插入圖片描述
在這裏插入圖片描述

(3)冒泡排序法示例

/*冒泡排序*/
public class BubbleSort {
    public static void main(String[] args) {
        /*隨機定義一個數組*/
        int arr[] = {7,3,8,2,6,4,9,1,5};
        System.out.println("排序前的數組arr[] = " + Arrays.toString(arr));

        /*獲取數組的長度*/
        int len = arr.length;
        /*第一層循環,代表要循環的次數*/
        for(int i = 0; i < len; i++){
            /*第二次循環,代表每次循環中排序的次數,每一次循環的排序次數會減少1*/
            for (int j = 0; j < len-1-i; j++){
                if(arr[j] > arr[j+1]){
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
            System.out.println("第" + (i+1) + "次循環排序後的結果爲arr[] = " + Arrays.toString(arr));
        }
        System.out.println("排序完成後的數組arr[] = " + Arrays.toString(arr));
    }
}

--------
輸出結果爲
排序前的數組arr[] = [7, 3, 8, 2, 6, 4, 9, 1, 5]1次循環排序後的結果爲arr[] = [3, 7, 2, 6, 4, 8, 1, 5, 9]2次循環排序後的結果爲arr[] = [3, 2, 6, 4, 7, 1, 5, 8, 9]3次循環排序後的結果爲arr[] = [2, 3, 4, 6, 1, 5, 7, 8, 9]4次循環排序後的結果爲arr[] = [2, 3, 4, 1, 5, 6, 7, 8, 9]5次循環排序後的結果爲arr[] = [2, 3, 1, 4, 5, 6, 7, 8, 9]6次循環排序後的結果爲arr[] = [2, 1, 3, 4, 5, 6, 7, 8, 9]7次循環排序後的結果爲arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]8次循環排序後的結果爲arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]9次循環排序後的結果爲arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
排序完成後的數組arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]

(4)代碼優化
因爲排序的過程中,各元素不斷接近自己的位置,如果一趟比較下來沒有進行過交換,就說明序列有序,因此要在排序過程中設置一個標誌flag判斷元素是否進行過交換。從而減少不必要的比較。

/*冒泡排序優化*/
public class BubbleSort {
    public static void main(String[] args) {
        /*隨機定義一個數組*/
        int arr[] = {1,6,2,7,3,8,4,5,9};
        System.out.println("排序前的數組arr[] = " + Arrays.toString(arr));

        /*獲取數組的長度*/
        int len = arr.length;
        /*定義一個標記,默認爲false*/
        boolean flag = false;
        /*第一層循環,代表要循環的次數*/
        for(int i = 0; i < len; i++){
            /*第二次循環,代表每次循環中排序的次數,每一次循環的排序次數會減少1*/
            for (int j = 0; j < len-1-i; j++){
                if(arr[j] > arr[j+1]){
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                    //改變標記
                    flag = true;
                }
            }
            if(flag == false){
                break;
            }else{
                /*重置標記,便於下次循環*/
                flag = false;
            }
            System.out.println("第" + (i+1) + "次循環排序後的結果爲arr[] = " + Arrays.toString(arr));
        }
        System.out.println("排序完成後的數組arr[] = " + Arrays.toString(arr));
    }
}

--------
輸出結果爲
排序前的數組arr[] = [1, 6, 2, 7, 3, 8, 4, 5, 9]1次循環排序後的結果爲arr[] = [1, 2, 6, 3, 7, 4, 5, 8, 9]2次循環排序後的結果爲arr[] = [1, 2, 3, 6, 4, 5, 7, 8, 9]3次循環排序後的結果爲arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
排序完成後的數組arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]

(5)冒泡排序分析
冒泡排序法分析的平均時間複雜度爲O(n^2),最好的情況下時間複雜度爲O(n),最壞的情況下時間複雜度爲O(n^2),空間複雜度爲O(1),是一種穩定的排序方式

2、選擇排序法

(1)選擇排序法介紹
選擇排序法(Selection sort)是一種簡單直觀的排序算法。它的工作原理是:第一次從待排序的數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,然後再從剩餘的未排序元素中尋找到最小(大)元素,然後放到已排序的序列的末尾。以此類推,直到全部待排序的數據元素的個數爲零。選擇排序是不穩定的排序方法。

(2)選擇排序法圖解
在這裏插入圖片描述
在這裏插入圖片描述

(3)選擇排序法示例

/*選擇排序*/
public class SelectSort {
    public static void main(String[] args) {
        /*定義一個數組*/
        int arr[] = {7,4,8,2,9,3,6,1,5};
        System.out.println("排序前的數組arr[] = " + Arrays.toString(arr));
        /*獲取數組的長度*/
        int len = arr.length;

        /*排序(從小到大)*/
        /*第一個循環代表要循環的次數*/
        for (int i = 0; i < len - 1; i++){
            /*默認第i個數爲最小值,其下表爲最小值對應的下標*/
            int minIndex = i;
            /*第二個循環代表每次循環中比較的次數,每輪需要比較的次數 N-i*/
            for (int j = i + 1; j < len; j++){
                if(arr[minIndex] > arr[j]){
                    /*得到當前最小值對應的下標*/
                    minIndex = j;
                }
            }
            /*數值交換*/
            if(minIndex != i){
                int temp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = temp;
            }
            System.out.println("第"+(i+1)+"排序後的數組arr[] = " + Arrays.toString(arr));
        }
        System.out.println("排序完成後的數組arr[] = " + Arrays.toString(arr));
    }
}

---------------
輸出結果爲
排序前的數組arr[] = [7, 4, 8, 2, 9, 3, 6, 1, 5]1排序後的數組arr[] = [1, 4, 8, 2, 9, 3, 6, 7, 5]2排序後的數組arr[] = [1, 2, 8, 4, 9, 3, 6, 7, 5]3排序後的數組arr[] = [1, 2, 3, 4, 9, 8, 6, 7, 5]4排序後的數組arr[] = [1, 2, 3, 4, 9, 8, 6, 7, 5]5排序後的數組arr[] = [1, 2, 3, 4, 5, 8, 6, 7, 9]6排序後的數組arr[] = [1, 2, 3, 4, 5, 6, 8, 7, 9]7排序後的數組arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]8排序後的數組arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
排序完成後的數組arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]

(4)選擇排序法分析
選擇排序法的平均時間複雜度爲O(n^2),最好的情況下時間複雜度爲O(n^2),最壞的情況下時間複雜度爲O(n^2),空間複雜度爲O(1),是一種不穩定的排序方式

3、插入排序法

(1)插入排序法介紹
插入式排序屬於內部排序法,是對於欲排序的元素以插入的方式找尋該元素的
適當位置,以達到排序的目的。

(2)插入排序法圖解
在這裏插入圖片描述
在這裏插入圖片描述
(3)插入排序法示例
第一種寫法

/*插入排序*/
public class InsertSort {
    public static void main(String[] args) {
        /*定義一個數組*/
        int arr[] = {7,4,8,2,9,3,6,1,5};
        System.out.println("排序前的數組arr[] = " + Arrays.toString(arr));
        /*獲取數組的長度*/
        int len = arr.length;

        /*排序(從小到大)*/
        /*第一個循環代表要循環的次數*/
        for (int i = 1; i < len; i++){
            /*取出每次循環時要進行比較插入的數據*/
            int temp = arr[i];
            /*定義一個index,代表要插入的位置的下標*/
            int index = i;
            /*第二個循環代表每次循環中比較的次數,每輪需要比較的次數 N-i*/
            for (int j = i; j > 0; j--){
                if(temp < arr[j-1]){
                    arr[j] = arr[j-1];
                    /*獲取要插入的下標*/
                    index--;
                }
            }
            /*插入數據*/
            if(index != i){
                arr[index] = temp;
            }
            System.out.println("第"+(i+1)+"排序後的數組arr[] = " + Arrays.toString(arr));
        }
        System.out.println("排序完成後的數組arr[] = " + Arrays.toString(arr));
    }
}

---------------
輸出結果爲
排序前的數組arr[] = [7, 4, 8, 2, 9, 3, 6, 1, 5]2排序後的數組arr[] = [4, 7, 8, 2, 9, 3, 6, 1, 5]3排序後的數組arr[] = [4, 7, 8, 2, 9, 3, 6, 1, 5]4排序後的數組arr[] = [2, 4, 7, 8, 9, 3, 6, 1, 5]5排序後的數組arr[] = [2, 4, 7, 8, 9, 3, 6, 1, 5]6排序後的數組arr[] = [2, 3, 4, 7, 8, 9, 6, 1, 5]7排序後的數組arr[] = [2, 3, 4, 6, 7, 8, 9, 1, 5]8排序後的數組arr[] = [1, 2, 3, 4, 6, 7, 8, 9, 5]9排序後的數組arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
排序完成後的數組arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]

第二種寫法

/*插入排序*/
public class InsertSort {
    public static void main(String[] args) {
        /*定義一個數組*/
        int arr[] = {7,4,8,2,9,3,6,1,5};
        System.out.println("排序前的數組arr[] = " + Arrays.toString(arr));
        /*獲取數組的長度*/
        int len = arr.length;

        /*排序(從小到大)*/
        /*第一個循環代表要循環的次數*/
        for (int i = 1; i < len; i++){
            /*取出每次循環時要進行比較插入的數據*/
            int temp = arr[i];
            /*定義一個index,代表要插入的位置的下標*/
            int index = i;
            /*第二個循環代表每次循環中比較的次數,每輪需要比較的次數 N-i*/
            while (index > 0 && temp < arr[index - 1]){
                arr[index] = arr[index - 1];
                index--;
            }
            /*插入數據*/
            arr[index] = temp;
            System.out.println("第"+(i+1)+"排序後的數組arr[] = " + Arrays.toString(arr));
        }
        System.out.println("排序完成後的數組arr[] = " + Arrays.toString(arr));
    }
}

----------
輸出結果爲
排序前的數組arr[] = [7, 4, 8, 2, 9, 3, 6, 1, 5]2排序後的數組arr[] = [4, 7, 8, 2, 9, 3, 6, 1, 5]3排序後的數組arr[] = [4, 7, 8, 2, 9, 3, 6, 1, 5]4排序後的數組arr[] = [2, 4, 7, 8, 9, 3, 6, 1, 5]5排序後的數組arr[] = [2, 4, 7, 8, 9, 3, 6, 1, 5]6排序後的數組arr[] = [2, 3, 4, 7, 8, 9, 6, 1, 5]7排序後的數組arr[] = [2, 3, 4, 6, 7, 8, 9, 1, 5]8排序後的數組arr[] = [1, 2, 3, 4, 6, 7, 8, 9, 5]9排序後的數組arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
排序完成後的數組arr[] = [1, 2, 3, 4, 5, 6, 7, 8, 9]

(4)插入排序法分析
插入排序法的平均時間複雜度爲O(n^2),最好的情況下時間複雜度爲O(n),最壞的情況下時間複雜度爲O(n^2),空間複雜度爲O(1),是一種穩定的排序方式

4、希爾排序法

(1)希爾排序法介紹

  • 希爾排序也稱遞減增量排序算法,是插入排序的一種更高效的改進版本,是希爾(Donald Shell)於1959年提出的一種排序算法。
  • 希爾排序是非穩定排序算法。
  • 希爾排序法基本思想
    希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序,隨着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個文件恰被分成一組,算法便終止。即先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄"基本有序"時,再對全體記錄進行依次直接插入排序。

(2)希爾排序法圖解
在這裏插入圖片描述
在這裏插入圖片描述

(3)希爾排序法示例

/*希爾排序法*/
public class ShellSort {
    public static void main(String[] args) {
        /*定義一個數組*/
        int arr[] = {5,0,7,2,9,4,6,1,8,3};
        System.out.println("排序前的數組arr[] = " + Arrays.toString(arr));
        /*獲取數組長度*/
        int len = arr.length;
        /*計數*/
        int count = 0;
        /*排序(從小到大)*/
        /*第一層循環,代表循環的次數*/
        for (int gap = len/2; gap > 0; gap /= 2){
            /*第二次循環,代表每次循環分組情況*/
            for (int i = gap; i < len; i++){
                /*第三次循環,代表具體比較情況*/
                for (int j = i - gap; j >= 0; j -= gap){
                    if(arr[j] > arr[j+gap]){
                        int temp = arr[j];
                        arr[j] = arr[j+gap];
                        arr[j+gap] = temp;
                    }
                }
            }
            count++;
            System.out.println("第"+count+"次排序後的數組arr[] = " + Arrays.toString(arr));
        }
        System.out.println("排序完成後的數組arr[] = " + Arrays.toString(arr));
    }
}

-------------
輸出結果爲
排序前的數組arr[] = [5, 0, 7, 2, 9, 4, 6, 1, 8, 3]1次排序後的數組arr[] = [4, 0, 1, 2, 3, 5, 6, 7, 8, 9]2次排序後的數組arr[] = [1, 0, 3, 2, 4, 5, 6, 7, 8, 9]3次排序後的數組arr[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
排序完成後的數組arr[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

(4)希爾排序法分析
希爾排序法的平均時間複雜度爲O(n log n),最好的情況下時間複雜度爲O(n log^2 n),最壞的情況下時間複雜度爲O(n log^2 n),空間複雜度爲O(1),是一種不穩定的排序方式。

5、快速排序法

(1)快速排序法介紹
快速排序(Quicksort) 是對冒泡排序的一種改進。基本思想是:通過-趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。

(2)快速排序法圖解
在這裏插入圖片描述
在這裏插入圖片描述

(3)快速排序法示例

/*快速排序法*/
public class QuickSort {
    public static void main(String[] args) {
        /*定義一個數組*/
        int arr[] = {5,0,7,2,9,4,6,1,8,3};
        System.out.println("排序前的數組arr[] = " + Arrays.toString(arr));
        /*獲取數組的長度*/
        int len = arr.length;

        /*遞歸排序*/
        quickSort(arr, 0, len - 1);
        System.out.println("排序完成後的數組arr[] = " + Arrays.toString(arr));

    }
    /*快速排序*/
    public static void quickSort(int[] arr,int l, int r){
        /*left爲左下標,right爲右下標,min爲中間值*/
        int left = l;
        int right = r;
        int min = arr[(l + r)/2];

        /*第一層循環:把比min大的梵高min右邊,比min小的放到min左邊*/
        while(left < right){
            /*在min的左邊找到大於等於min的值對應的下標*/
            while(arr[left] < min){
                left += 1;
            }
            /*在min的右邊邊找到小於等於min的值對應的下標*/
            while (arr[right] > min){
                right -= 1;
            }
            /*如果left>=right說明min左邊的值已經全部是小於min的,
                右邊的值已經全部是大於min的*/
            if(left >= right){
                break;
            }
            /*交換比較的值*/
            int temp = arr[left];
            arr[left] = arr[right];
            arr[right] = temp;

            //如果交換完後,發現這個arr[left] == min值相等right--,前移
            if(arr[left] == min) {
                right -= 1;
            }
            //如果交換完後,發現這個arr[right] == min值相等1eft++,後移
            if(arr[right] == min) {
                left += 1;
            }
        }
        /*防止棧內存溢出*/
        if(left == right){
            left += 1;
            right -= 1  ;
        }
        /*向左遞歸*/
        if(l < right){
            quickSort(arr,l,right);
        }
        /*向左遞歸*/
        if(left < r){
            quickSort(arr,left,r);
        }
    }
}

------------
輸出結果
排序前的數組arr[] = [5, 0, 7, 2, 9, 4, 6, 1, 8, 3]
排序完成後的數組arr[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

(4)快速排序法分析
快速排序法的平均時間複雜度爲O(n log n),最好的情況下時間複雜度爲O(n log n),最壞的情況下時間複雜度爲O(n^2),空間複雜度爲O(log n),是一種不穩定的排序方式。

6、歸併排序法

(1)歸併排序法介紹

  • 歸併排序(MERGE- SORT)是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。
  • 分治法將問題分成一些小的問題然後遞歸求解,而治(conquer)的階段則將分的階段得到的各答案修補”在一起,即分而治之。

(2)歸併排序法圖解
在這裏插入圖片描述
在這裏插入圖片描述

(3)歸併排序法示例

/*歸併排序*/
public class MergeSort {
    public static void main(String[] args) {
        /*定義一個數組*/
        int arr[] = {5,0,7,2,9,4,6,1,8,3};
        System.out.println("排序前的數組arr[] = " + Arrays.toString(arr));
        /*獲取數組的長度*/
        int len = arr.length;
        int tempArr[] = new int[len];

        mergeSort(arr,0,len - 1,tempArr);
        System.out.println("排序完成後的數組arr[] = " + Arrays.toString(arr));
    }

    /*分解合併*/
    public static void mergeSort(int[] arr,int left, int right, int[] tempArr){
        if(left < right){
            int mid = (left + right) / 2;
            /*向左遞歸分解*/
            mergeSort(arr,left,mid,tempArr);
            /*向右遞歸分解*/
            mergeSort(arr,mid + 1,right,tempArr);

            /*合併*/
            merge(arr,left,mid,right,tempArr);
        }
    }

    /*合併*/
    public static void merge(int[] arr,int left, int mid, int right, int[] tempArr){
        /*left是左邊有序序列的初始索引,min是中間有序序列的初始索引,
            right是右邊有序序列的初始索引,tempArr是中間臨時中轉時用的數組*/
        int l = left;
        int r = mid + 1;
        int index = 0;  //指向tempArr數組的當前索引

        /*把左右兩邊有序的數組按照規則填充到tempArr數組,直到左右兩邊有一邊處理完畢爲止*/
        while(l <= mid && r <= right){
            /*如果左邊的有序序列的當前元素值小於等於右邊有序序列的當前元素值,
            	就將左邊的拷貝到tempArr數組中*/
            if(arr[l] <= arr[r]){
                tempArr[index] = arr[l];
                index += 1;
                l += 1;
            }else{
                /*如果左邊的有序序列的當前元素值大於等於右邊有序序列的當前元素值,
                	就將右邊的拷貝到tempArr數組中*/
                tempArr[index] = arr[r];
                index += 1;
                r += 1;
            }
        }

        /*把有剩餘數據的一邊的數據一次全部填充到tempArr數組中*/
        /*將左邊有剩餘的數據填充到tempArr數組*/
        while(l <= mid){
            tempArr[index] = arr[l];
            index += 1;
            l += 1;
        }
        /*將右邊有剩餘的數據填充到tempArr數組*/
        while(r <= right){
            tempArr[index] = arr[r];
            index += 1;
            r += 1;
        }

        /*將臨時數組tempArr數組中的元素拷貝到arr數組*/
        int index1 = 0;
        int tempLeft = left;
        while(tempLeft <= right){
            arr[tempLeft] = tempArr[index1];
            index1 += 1;
            tempLeft += 1;
        }
    }
}

----------
輸出結果爲
排序前的數組arr[] = [5, 0, 7, 2, 9, 4, 6, 1, 8, 3]
排序完成後的數組arr[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

(4)歸併排序法分析
歸併排序法的平均時間複雜度爲O(n log n),最好的情況下時間複雜度爲O(n log n),最壞的情況下時間複雜度爲O(n log n),空間複雜度爲O(n),是一種穩定的排序方式。

7、堆排序法

(1)堆排序法介紹
堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。堆排序可以說是一種利用堆的概念來排序的選擇排序。分爲兩種方法:

  • 大頂堆:每個節點的值都大於或等於其子節點的值,在堆排序算法中用於升序排列;
  • 小頂堆:每個節點的值都小於或等於其子節點的值,在堆排序算法中用於降序排列;

堆排序的平均時間複雜度爲 Ο(nlogn)。

(2)堆排序法圖解
在這裏插入圖片描述
在這裏插入圖片描述

(3)堆排序法示例

/*堆排序*/
public class HeapSort {
    public static void main(String[] args) {
        /*定義一個數組*/
        int arr[] = {5,0,7,2,9,4,6,1,8,3};
        System.out.println("排序前的數組arr[] = " + Arrays.toString(arr));
        /*獲取數組的長度*/
        int len = arr.length;

        buildMaxHeap(arr, len);
        System.out.println("排序完成後的數組arr[] = " + Arrays.toString(arr));
    }

    public static void buildMaxHeap(int[] arr, int len) {
        for (int i = len / 2; i >= 0; i--) {
            heapify(arr, i, len);
        }
        for (int i = len - 1; i > 0; i--) {
            int temp = arr[0];
            arr[0] = arr[i];
            arr[i] = temp;
            len--;
            heapify(arr, 0, len);
        }
    }

    public static void heapify(int[] arr, int i, int len) {
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        int largest = i;

        if (left < len && arr[left] > arr[largest]) {
            largest = left;
        }

        if (right < len && arr[right] > arr[largest]) {
            largest = right;
        }

        if (largest != i) {
            int temp = arr[i];
            arr[i] = arr[largest];
            arr[largest] = temp;
            heapify(arr, largest, len);
        }
    }
}

----------
輸出結果爲
排序前的數組arr[] = [5, 0, 7, 2, 9, 4, 6, 1, 8, 3]
排序完成後的數組arr[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

(4)堆排序法分析
堆排序法的平均時間複雜度爲O(n log n),最好的情況下時間複雜度爲O(n log n),最壞的情況下時間複雜度爲O(n log n),空間複雜度爲O(1),是一種不穩定的排序方式。

8、計數排序法

(1)計數排序法介紹

  • 計數排序的核心在於將輸入的數據值轉化爲鍵存儲在額外開闢的數組空間中。作爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是有確定範圍的整數。

  • 計數排序的特徵

    • 當輸入的元素是 n 個 0 到 k 之間的整數時,它的運行時間是 Θ(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序算法。
    • 由於用來計數的數組C的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上1),這使得計數排序對於數據範圍很大的數組,需要大量時間和內存。例如:計數排序是用來排序0到100之間的數字的最好的算法,但是它不適合按字母順序排序人名。但是,計數排序可以用在基數排序中的算法來排序數據範圍很大的數組。
    • 通俗地理解,例如有 10 個年齡不同的人,統計出有 8 個人的年齡比 A 小,那 A 的年齡就排在第 9 位,用這個方法可以得到其他每個人的位置,也就排好了序。當然,年齡有重複時需要特殊處理(保證穩定性),這就是爲什麼最後要反向填充目標數組,以及將每個數字的統計減去 1 的原因。
  • 算法的步驟如下:

    • 找出待排序的數組中最大和最小的元素
    • 統計數組中每個值爲i的元素出現的次數,存入數組C的第i項
    • 對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加)
    • 反向填充目標數組:將每個元素i放在新數組的第C(i)項,每放一個元素就將C(i)減去1

(2)計數排序法圖解
在這裏插入圖片描述

(3)計數排序法示例

/*計數排序*/
public class CountSort {
    public static void main(String[] args) {
        /*定義一個數組*/
        int arr[] = {5,0,7,2,9,4,6,1,8,3};
        System.out.println("排序前的數組arr[] = " + Arrays.toString(arr));

        countingSort(arr);
        System.out.println("排序後的數組arr[] = " + Arrays.toString(arr));
    }

    private static void countingSort(int[] arr) {
        /*假定最大值爲arr[0]*/
        int maxValue = arr[0];
        /*找到最大值*/
        for (int value : arr) {
            if (maxValue < value) {
                maxValue = value;
            }
        }

        /*定義一個長度比最大值大一的數組用於排序*/
        int[] bucket = new int[maxValue + 1];

        /*遍歷原始數組中的每一個值,
            並將新的排序數組中對應的索引中存儲值+1用於排序數組*/
        for (int value : arr) {
            bucket[value]++;
        }

        int sortedIndex = 0;
        for (int j = 0; j < bucket.length; j++) {
            while (bucket[j] > 0) {
                arr[sortedIndex++] = j;
                bucket[j]--;
            }
        }
    }
}

----------
輸出結果爲
排序前的數組arr[] = [5, 0, 7, 2, 9, 4, 6, 1, 8, 3]
排序後的數組arr[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

(4)計數排序法分析
計數排序法的平均時間複雜度爲O(n+k),最好的情況下時間複雜度爲O(n+k),最壞的情況下時間複雜度爲O(n+k),空間複雜度爲O(k),是一種穩定的排序方式。

9、桶排序法

(1)介紹
桶排序是計數排序的升級版。它利用了函數的映射關係,高效與否的關鍵就在於這個映射函數的確定。爲了使桶排序更加高效,我們需要做到這兩點:

  • 在額外空間充足的情況下,儘量增大桶的數量
  • 使用的映射函數能夠將輸入的 N 個數據均勻的分配到 K 個桶中

同時,對於桶中元素的排序,選擇何種比較排序算法對於性能的影響至關重要。

當輸入的數據可以均勻的分配到每一個桶中時,桶排序效率最快;當輸入的數據被分配到了同一個桶中時,桶排序效率最慢。

(2)圖解
元素分佈在各個桶中:
在這裏插入圖片描述
在每個桶中分別對元素進行排序
在這裏插入圖片描述

(3)示例

/*桶排序*/
public class BucketSort {
    public static void main(String[] args) {
        /*定義一個數組*/
        int arr[] = {5,0,7,2,9,4,6,1,8,3};
        System.out.println("排序前的數組arr[] = " + Arrays.toString(arr));

        bucketSort(arr, 5);
        System.out.println("排序後的數組arr[] = " + Arrays.toString(arr));
    }

    private static void bucketSort(int[] arr, int bucketSize){
        if (arr.length != 0) {
            int minValue = arr[0];
            int maxValue = arr[0];
            for (int value : arr) {
                if (value < minValue) {
                    minValue = value;
                } else if (value > maxValue) {
                    maxValue = value;
                }
            }

            int bucketCount = (maxValue - minValue) / bucketSize + 1;
            int[][] buckets = new int[bucketCount][0];

            // 利用映射函數將數據分配到各個桶中
            for (int i = 0; i < arr.length; i++) {
                int index = (arr[i] - minValue) / bucketSize;
                buckets[index] = arrAppend(buckets[index], arr[i]);
            }

            int arrIndex = 0;
            for (int[] bucket : buckets) {
                if (bucket.length <= 0) {
                    continue;
                }
                // 對每個桶進行排序,這裏使用了插入排序
                bucket = insertSort(bucket);
                for (int value : bucket) {
                    arr[arrIndex++] = value;
                }
            }
        }
    }

    /*自動擴容,並保存數據*/
    private static int[] arrAppend(int[] arr, int value) {
        arr = Arrays.copyOf(arr, arr.length + 1);
        arr[arr.length - 1] = value;
        return arr;
    }

    /*插入排序*/
    public static int[] insertSort(int[] arr){
        int len = arr.length;
        for (int i = 1; i < len; i++){
            int temp = arr[i];
            int index = i;
            for (int j = i; j > 0; j--){
                if(temp < arr[j-1]){
                    arr[j] = arr[j-1];
                    index--;
                }
            }
            if(index != i){
                arr[index] = temp;
            }
        }
        return arr;
    }
}

----------
輸出結果爲
排序前的數組arr[] = [5, 0, 7, 2, 9, 4, 6, 1, 8, 3]
排序後的數組arr[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

(4)分析
桶排序法的平均時間複雜度爲O(n+k),最好的情況下時間複雜度爲O(n+k),最壞的情況下時間複雜度爲O(n^2),空間複雜度爲O(n+k),是一種穩定的排序方式。

10、基數排序法

(1)介紹

  • 基數排序是1887年赫爾曼.何樂禮發明的,它是這樣實現的:將整數按位數切割成不同的數字,然後按每個位數分別比較。
  • 基數排序(Radix Sort)屬於“分配式排序”(DistributionDort) ,又稱“桶子法”( Bucket Sort)或BinSort,是桶排序的擴展, 顧名思義,它是通過鍵值的各個位的值,將要排序的元素分配至某些“桶”中,達到排序的作用。
  • 基數排序法屬於穩定性的排序,是效率高的穩定性排序法。
  • 基數排序的基本思想
    將所有待比較數值統-一爲同樣的數位長度,數位較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後,數列就變成一個有序序列。

(2)圖解
在這裏插入圖片描述
在這裏插入圖片描述

(3)示例

/*基數排序法*/
public class RadixSort {
    public static void main(String[] args) {
        /*定義一個數組*/
        int arr[] = {163,527,369,3,17,9,63,7,59,123};
        System.out.println("排序前的數組arr[] = " + Arrays.toString(arr));

        radixSort(arr);
        System.out.println("排序後的數組arr[] = " + Arrays.toString(arr));
    }

    public static void radixSort(int[] arr){
        /*假定最大值爲arr[0]*/
        int max = arr[0];
        /*找到數組中實際的最大值*/
        for(int i = 1; i < arr.length; i++){
            if(arr[i] > max){
                max = arr[i];
            }
        }
        /*計算最大值是多少位數*/
        int maxLen = (max + "").length();

        /*定義一個二位數組,表示10個桶,每個桶中存放相應數據*/
        int[][] bucket = new int[10][arr.length];

        /*定義一個一維數組,用於記錄每個桶每次放入的元素個數*/
        int[] count = new int[10];

        /*n每次循環完會乘以10,表示到了數值那一位(個位、十位、百位...)*/
        int n = 1;
        /*第一個循環:最大值有多少位數就進行幾次循環*/
        for(int i = 0; i < maxLen; i++){
            /*第二輪循環:根據每個元素對應的位數的值進行排序*/
            for(int j = 0; j < arr.length; j++){
                /*去除每個元素的個位的值*/
                int element = arr[j] /n % 10;
                /*將元素放入到個位對應的桶中*/
                bucket[element][count[element]] = arr[j];
                count[element]++;
            }

            /*按照桶的順序和一維數組的下標順序依次取出數據放入到原來的數組中*/
            int index = 0;
            /*遍歷每個桶並將桶中的數據取出放入到原數組*/
            for(int k = 0; k < count.length; k++){
                /*判斷該桶是否有數據*/
                if(count[k] != 0){
                    /*循環第k個桶中的數據*/
                    for (int m = 0; m < count[k]; m++){
                        arr[index] = bucket[k][m];
                    index++;
                    }
                }
                /*每輪循環後,將桶中的元素清空*/
                count[k] = 0;
            }
            n *= 10;
            System.out.println("第" + (i+1) + "輪循環排序後數組arr[] = " + Arrays.toString(arr));
        }
    }
}

----------
輸出結果爲
排序前的數組arr[] = [163, 527, 369, 3, 17, 9, 63, 7, 59, 123]1輪循環排序後數組arr[] = [163, 3, 63, 123, 527, 17, 7, 369, 9, 59]2輪循環排序後數組arr[] = [3, 7, 9, 17, 123, 527, 59, 163, 63, 369]3輪循環排序後數組arr[] = [3, 7, 9, 17, 59, 63, 123, 163, 369, 527]
排序後的數組arr[] = [3, 7, 9, 17, 59, 63, 123, 163, 369, 527]

(4)分析

  • 基數排序法的平均時間複雜度爲O(n*k),最好的情況下時間複雜度爲O(n*k),最壞的情況下時間複雜度爲O(n*k),空間複雜度爲O(n+k),是一種穩定的排序方式。
  • 基數排序是對傳統桶排序的擴展,速度很快。
  • 基數排序是經典的空間換時間的方式,佔用內存很大,當對海量數據排序時,容易造成OutOfMemoryError。
  • 基數排序是穩定的。[注 :假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[i], 且r[i]在rj]之 前,而在排序後的序列中,r[i]仍在r[]之前,則稱這種排序算法是穩定的,否則稱爲不穩定的。

(5)計數排序 、桶排序、基數排序比較
這三種排序算法都利用了桶的概念,但對桶的使用方法上有明顯差異:

  • 基數排序:根據鍵值的每位數字來分配桶;
  • 計數排序:每個桶只存儲單一鍵值;
  • 桶排序:每個桶存儲一定範圍的數值。

三、排序方式彙總比較

在這裏插入圖片描述
名詞解釋:

  • n:數據規模
  • k:"桶"的個數
  • In-place:佔用常數內存,不佔用額外內存
  • Out-place:佔用額外內存
  • 穩定性:排序後 2 個相等鍵值的順序和排序之前它們的順序相同,如a,b兩個數,如果a=b,排序後順序依舊是a,b。
  • 不穩定性:排序後 2 個相等鍵值的順序和排序之前它們的順序不相同,如a,b兩個數,如果a=b,排序後順序有可能是b,a。
  • 時間複雜度:一個算法執行所耗費的時間。
  • 空間複雜度:運行完一個程序需要的內存大小。
  • 內排序:所有排序操作都在內存中進行。
  • 外排序:由於數據太大,因此把數據放在磁盤中,而排序通過磁盤和內存的數據傳輸才能進行。

在這裏插入圖片描述
總結:

  • 平方階 (O(n2)) 排序 各類簡單排序:直接插入、直接選擇和冒泡排序。
  • 線性對數階 (O(nlog2n)) 排序 快速排序、堆排序和歸併排序。
  • O(n1+§)) 排序,§ 是介於 0 和 1 之間的常數。 希爾排序。
  • 線性階 (O(n)) 排序 基數排序,此外還有桶、箱排序。
  • 穩定的排序算法:冒泡排序、插入排序、歸併排序和基數排序
  • 不是穩定的排序算法:選擇排序、快速排序、希爾排序、堆排序。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章