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

1、插入類排序

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

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 時,希爾排序就是插入排序,因此希爾排序最重要的就是增量的選取java

主要步驟是:git

    1. 將待排序數組按照初始增量 d 進行分組
    1. 在每一個組中對元素進行直接插入排序
    1. 將增量 d 折半,循環 一、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)

2、交換類排序

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

3. 冒泡排序

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

/**
* 冒泡排序
*/
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)

3、選擇排序

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

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.堆排序

堆是一種數據結構,能夠把堆當作一顆徹底二叉樹,並且這棵樹任何一個非葉結點的值都不大於(或不小於)其左右孩子結點的值。若父結點大子結點小,則這樣的堆叫作大頂堆;若父結點小子結點大,則這樣的堆叫作小頂堆。函數

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

排序主要步驟能夠分爲(以大頂堆爲例):指針

(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)

4、其餘內部排序

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. 基數排序就這麼簡單
相關文章
相關標籤/搜索