用 Java 實現常見的 8 種內部排序演算法

Ethan_Wong發表於2021-08-11

一、插入類排序

插入類排序就是在一個有序的序列中,插入一個新的關鍵字。從而達到新的有序序列。插入排序一般有直接插入排序、折半插入排序和希爾排序。

1. 插入排序

1.1 直接插入排序

/**
* 直接比較,將大元素向後移來移動陣列
*/
public static void InsertSort(int[] A) {
    for(int i = 1; i < A.length; i++) {
        int temp = A[i];						   //temp 用於儲存元素,防止後面移動陣列被前一個元素覆蓋
        int j;
        for(j = i; j > 0 && temp < A[j-1]; j--) { //如果 temp 比前一個元素小,則移動陣列
            A[j] = A[j-1];
        }
        A[j] = temp;							  //如果 temp 比前一個元素大,遍歷下一個元素
    }
}

/**
* 這裡是通過類似於冒泡交換的方式來找到插入元素的最佳位置。而傳統的是直接比較,移動陣列元素並最後找到合適的位置
*/
public static void InsertSort2(int[] A) { //A[] 是給定的待排陣列
    for(int i = 0; i < A.length - 1; i++) {   //遍歷陣列
        for(int j = i + 1; j > 0; j--) { //在有序的序列中插入新的關鍵字
            if(A[j] < A[j-1]) {          //這裡直接使用交換來移動元素
                int temp = A[j];
                A[j] = A[j-1];
                A[j-1] = temp;
            }
        }
    }
}

/**
* 時間複雜度:兩個 for 迴圈 O(n^2) 
* 空間複雜度:佔用一個陣列大小,屬於常量,所以是 O(1)
*/

1.2 折半插入排序

/*
* 從直接插入排序的主要流程是:1.遍歷陣列確定新關鍵字 2.在有序序列中尋找插入關鍵字的位置
* 考慮到陣列線性表的特性,採用二分法可以快速尋找到插入關鍵字的位置,提高整體排序時間
*/
public static void BInsertSort(int[] A) {
    for(int i = 1; i < A.length; i++) {
        int temp = A[i];
        //二分法查詢
        int low = 0;
        int high = i - 1;
        int mid;
        while(low <= high) {
            mid = (high + low)/2;
            if (A[mid] > temp) {
                high = mid - 1;
            } else {
                low = mid + 1;
            }
        }
        //向後移動插入關鍵字位置後的元素
        for(int j = i - 1; j >= high + 1; j--) {
            A[j + 1] = A[j];
        }
        //將元素插入到尋找到的位置
        A[high + 1] = temp;
    }
}

2. 希爾排序

希爾排序又稱縮小增量排序,其本質還是插入排序,只不過是將待排序列按某種規則分成幾個子序列,然後如同前面的插入排序一般對這些子序列進行排序。因此當增量為 1 時,希爾排序就是插入排序,所以希爾排序最重要的就是增量的選取

主要步驟是:

    1. 將待排序陣列按照初始增量 d 進行分組
    1. 在每個組中對元素進行直接插入排序
    1. 將增量 d 折半,迴圈 1、2 、3步驟
    1. 待 d = 1 時,最後一次使用直接插入排序完成排序

/**
* 希爾排序的實現程式碼還是比較簡潔的,除了增量的變化,基本上和直接插入序列沒有區別
*/
public static void ShellSort(int[] A) {
    for(int d = A.length/2; d >= 1; d = d/2) {     //增量的變化,從 d = "陣列長度一半"到 d = 1
        for(int i = d; i < A.length; i++) {        //在一個增量範圍內進行遍歷[d,A.length-1]
            if(A[i] < A[i - d]) {				   //若增量後的元素小於增量前的元素,進行插入排序
                int temp = A[i];
                int j;
                for(j = i - d; j >= 0 && temp < A[j-d]; j -= d) { //對該增量序列下的元素進行排序
                    A[j + d] = A[j]; 				//這裡要使用i + d 的方式來移動元素,因為增量 d 可能大於陣列下標
                }									//造成陣列序列超出陣列的範圍
                A[j + d] = temp;
            }
        }
    }
}

複雜度分析

排序方法 空間複雜度 最好情況 最壞情況 平均時間複雜度
直接插入排序 O(1) O(n^2) O(n^2) O(n^2)
折半插入排序 O(1) O(nlog2n) O(n^2) O(n^2)
希爾排序 O(1) O(nlog2n) O(nlog2n) O(nlog2n)

二、交換類排序

交換,指比較兩個元素關鍵字大小,來交換兩個元素在序列中的位置,最後達到整個序列有序的狀態。主要有氣泡排序和快速排序

3. 氣泡排序

氣泡排序就是通過依次比較序列中兩個相鄰元素的值,根據需要的升降序來交換這兩個元素。最終達到整個序列有序的結果。

/**
* 氣泡排序
*/
public static void BubbleSort(int[] A) {
    for (int i = 0; i < A.length - 1; i++) {        //冒泡次數,遍歷陣列次數,有序元素個數
        for(int j = 0; j < A.length - i - 1; j++) { //對剩下無序元素進行交換排序
            if(A[j] > A[j + 1]) {
                int temp = A[j];
                A[j] = A[j + 1];
                A[j + 1] = temp;
            }
        }
    }
}

4. 快速排序

快速排序實際上也是屬於交換類的排序,只是它通過多次劃分操作實現排序。這就是分治思想,把一個序列分成兩個子序列它每一趟選擇序列中的一個關鍵字作為樞軸,將序列中比樞軸小的移到前面,大的移到後邊。當本趟所有子序列都被樞軸劃分完畢後得到一組更短的子序列,成為下一趟劃分的初始序列集。每一趟結束後都會有一個關鍵字達到最終位置。

 /**
     * 快速排序算是在氣泡排序的基礎上的遞迴分治交換排序
     * @param A 待排陣列
     * @param low 陣列起點
     * @param high 陣列終點
     */
    public static void QuickSort(int[] A, int low, int high) {
        if(low >= high) {                             //遞迴分治完成退出
            return;
        }
        int left = low;                               //設定左遍歷指標 left
        int right = high;                             //設定右遍歷指標 right
        int pivot = A[left];                          //設定樞軸 pivot, 預設是陣列最左端的值
        while(left < right) {                         //迴圈條件
            while(left < right && A[right] >= pivot) {//若右指標所指向元素大於樞軸值,則右指標向左移動
                right--;
            }
            A[left] = A[right];                       //反之替換
            while (left < right && A[left] <= pivot) {//若左指標所指向元素小於樞軸值,則左指標向右移動
                left++;
            }
            A[right] = A[left];                       //反之替換
        }
        A[left] = pivot;                              //將樞軸值放在最終位置上
        QuickSort(A, low, left - 1);            //依此遞迴樞軸值左側的元素
        QuickSort(A, left + 1, high);            //依此遞迴樞軸值右側的元素
    }

複雜度分析

排序方法 空間複雜度 最好情況 最壞情況 平均時間複雜度
氣泡排序 O(1) O(n^2) O(n^2) O(n^2)
快速排序 O(log2n) O(nlog2n) O(n^2) O(nlog2n)

三、選擇排序

選擇排序就是每一趟從待排序列中選擇關鍵字最小的元素,直到待排序列元素選擇完畢。

5. 簡單選擇排序

/**
 * 簡單選擇排序
 * @param A 待排陣列
 */
public static void SelectSort(int [] A) {
    for (int i = 0; i < A.length; i++) {
        int min = i;                             //遍歷選擇序列中的最小值下標
        for (int j = i + 1; j < A.length; j++) { //遍歷當前序列選擇最小值
            if (A[j] < A[min]) {
                min = j;
            }
        }
        if (min != i) {                          //選擇並交換最小值
            int temp = A[min];
            A[min] = A[i];
            A[i] = temp;
        }
    }
}

6.堆排序

堆是一種資料結構,可以把堆看成一顆完全二叉樹,而且這棵樹任何一個非葉結點的值都不大於(或不小於)其左右孩子結點的值。若父結點大子結點小,則這樣的堆叫做大頂堆;若父結點小子結點大,則這樣的堆叫做小頂堆。

堆排序的過程實際上就是將堆排序的序列構造成一個堆,將堆中最大的取走,再將剩餘的元素調整成堆,然後再找出最大的取走。這樣重複直至取出的序列有序。

排序主要步驟可以分為(以大頂堆為例):

(1) 將待排序列構造成一個大頂堆:BuildMaxHeap()

(2) 對堆進行調整排序:AdjustMaxHeap()

(3) 進行堆排序,移除根結點,調整堆排序:HeapSort()

/**
     * 堆排序(大頂堆)
     * @param A 待排陣列
     */
public static void HeapSort(int [] A) {
    BuildMaxHeap(A);							//建立堆
    for (int i = A.length - 1; i > 0; i--) {	//排序次數,需要len - l 趟
        int temp = A[i];						//將堆頂元素(A[0])與陣列末尾元素替換,更新待排陣列長度
        A[i] = A[0];
        A[0] = temp;
        AdjustMaxHeap(A, 0, i);					//調整新堆,對未排序陣列再次進行調整
    }
}

/**
     * 建立大頂堆
     * @param A 待排陣列
     */
public static void BuildMaxHeap(int [] A) {
    for (int i = (A.length / 2) -1; i >= 0 ; i--) { //對[0,len/2]區間中的的結點(非葉結點)從下到上進行篩選調整
        AdjustMaxHeap(A, i, A.length);
    }
}

/**
     * 調整大頂堆
     * @param A 待排陣列
     * @param k 當前大頂堆根結點在陣列中的下標
     * @param len 當前待排陣列長度
     */
public static void AdjustMaxHeap(int [] A, int k, int len) { 
    int temp = A[k];
    for (int i = 2*k + 1; i < len; i = 2*i + 1) { //從最後一個葉結點開始從下到上進行堆調整
        if (i + 1 < len && A[i] < A[i + 1]) {     //比較兩個子結點大小,取其大值
            i++;
        }
        if (temp < A[i]) {						  //若結點大於父結點,將父結點替換
            A[k] = A[i];						  //更新陣列下標,繼續向上進行堆調整
            k = i;
        } else {
            break;								  //若該結點小於父結點,則跳過繼續向上進行堆調整
        }
    }
    A[k] = temp;								 //將結點放入比較後應該放的位置
}

複雜度分析

排序方法 空間複雜度 最好情況 最壞情況 平均時間複雜度
簡單選擇排序 O(1) O(n^2) O(n^2) O(n^2)
堆排序 O(1) O(log2n) O(nlog2n) O(nlog2n)

四、其他內部排序

7. 歸併排序

歸併排序是將多個有序表組合成一個新的有序表,該演算法是採用分治法的一個典型的應用。即把待排序列分為若干個子序列,每個子序列是有序的。然後再把有序子序列合併為一個整體有序的序列。這裡主要以二路歸併排序來進行分析。

該排序主要分為兩步:

    1. 分解:將序列每次折半拆分
    2. 合併:將劃分後的序列兩兩排序併合並

private static int[] aux;          
/**
     * 初始化輔助陣列 aux
     * @param A 待排陣列
     */
public static void MergeSort(int [] A) {
    aux = new int[A.length];      
    MergeSort(A,0,A.length-1);
}

/**
     * 將陣列分成兩部分,以陣列中間下標 mid 分為兩部分依此遞迴
     * 最後再將兩部分的有序序列通過 Merge() 函式 合併
      * @param A 待排陣列
     * @param low 陣列起始下標
     * @param high 陣列末尾下標
     */
public static void MergeSort (int[] A, int low, int high) {
    if (low < high) {
        int mid = (low + high) / 2;
        MergeSort(A, low, mid);
        MergeSort(A, mid + 1, high);
        Merge(A, low, mid, high);
    }
}

/**
     * 將 [low, mid] 有序序列和 [mid+1, high] 有序序列合併 
     * @param A 待排陣列
     * @param low 陣列起始下標
     * @param mid 陣列中間分隔下標
     * @param high 陣列末尾下標
     */
public static void Merge (int[] A, int low, int mid, int high) {
    int i, j, k;
    for (int t = low; t <= high; t++) {
        aux[t] = A[t];
    }
    for ( i = low, j = mid + 1, k = low; i <= mid && j <= high; k++) {
        if(aux[i] < aux[j]) {
            A[k] = aux[i++];
        } else {
            A[k] = aux[j++];
        }
    }
    while (i <= mid) {
        A[k++] = aux[i++];
    }
    while (j <= high) {
        A[k++] = aux[j++];
    }
}

8. 基數排序

基數排序比較特別,它是通過關鍵字數字各位的大小來進行排序。它是一種藉助多關鍵字排序的思想來對單邏輯關鍵字進行排序的方法。

它主要有兩種排序方法:

  • 最高位優先法(MSD):按照關鍵字位權重高低依此遞減來劃分子序列
  • 最低位優先法(LSD) :按照關鍵字位權重低高依此增加來劃分子序列

基數排序的思想:

  • 分配
  • 回收

/**
     * 找出陣列中的最長位數
     * @param A 待排陣列
     * @return MaxDigit 最長位數
     */
public static int MaxDigit (int [] A) {
    if (A == null) {
        return 0;
    }
    int Max = 0;
    for (int i = 0; i < A.length; i++) {
        if (Max < A[i]) {
            Max = A[i];
        }
    }
    int MaxDigit = 0;
    while (Max > 0) {
        MaxDigit++;
        Max /= 10;
    }
    return MaxDigit;
}

/**
     * 將基數排序的操作內化在一個二維陣列中進行
     * @param A 待排陣列
     */
public static void RadixSort(int [] A) {
    //建立一個二維陣列,類比於在直角座標系中,進行分配收集操作
    int[][] buckets = new int[10][A.length];
    int MaxDigit = MaxDigit(A);
    //t 用於提取關鍵字的位數
    int t = 10;
    //按排序趟數進行迴圈
    for (int i = 0; i < MaxDigit; i++) {
        //在一個桶中存放元素的數量,是buckets 二維陣列的y軸
        int[] BucketLen = new int[10];
        //分配操作:將待排陣列中的元素依此放入桶中
        for (int j = 0; j < A.length ; j++) {
            //桶的下標值,是buckets 二維陣列的x軸
            int BucketIndex = (A[j] % t) / (t / 10);
            buckets[BucketIndex][BucketLen[BucketIndex]] = A[j];
            //該下標下,也就是桶中元素個數隨之增加
            BucketLen[BucketIndex]++;
        }
        //收集操作:將已排好序的元素從桶中取出來
        int k = 0;
        for (int x = 0; x < 10; x++) {
            for (int y = 0; y < BucketLen[x]; y++) {
                A[k++] = buckets[x][y];
            }
        }
        t *= 10;
    }
}

複雜度分析

排序方法 空間複雜度 最好情況 最壞情況 平均時間複雜度
歸併排序 O(n) O(nlog2n) O(nlog2n) O(nlog2n)
基數排序 O(rd) O(d(n+rd)) O(d(n+rd)) O(d(n+rd))

備註:基數排序中,n 為序列中的關鍵字數,d為關鍵字的關鍵字位數,rd 為關鍵字位數的個數

參考文章:

  1. Java 實現八大排序演算法
  2. 《 2022王道資料結構》
  3. 《演算法》
  4. 八種排序演算法模板
  5. 基數排序就這麼簡單

相關文章