10大排序演算法——Java實現

城北有個混子發表於2020-11-21

演算法與實現

選擇排序

演算法思想

  從陣列中選擇最小元素,將它與陣列的第一個元素交換位置。再從陣列剩下的元素中選擇出最小的元素,將它與陣列的第二個元素交換位置。不斷進行這樣的操作,直到將整個陣列排序。

動態過程

  演算法原理參考:圖解選擇排序

程式碼實現

// 選擇排序
public static void selectSort(int[] arr) {
    for (int i = 0; i < arr.length-1; i++) {
        int k = i;
        for (int j = i+1; j < arr.length; j++) {
            if (arr[j] < arr[k]) {
                k = j;
            }
        }
        if (k != i) {
            int temp = arr[i];
            arr[i] = arr[k];
            arr[k] = temp;
        }
    }
}

 


氣泡排序

演算法思想

  從左到右不斷交換相鄰逆序的元素,在一輪的迴圈之後,可以讓未排序的最大元素上浮到右側。在一輪迴圈中,如果沒有發生交換,那麼說明陣列已經是有序的,此時可以直接退出。

  演算法原理參考:圖解氣泡排序

程式碼實現

// 氣泡排序
public static void bubbleSort(int[] arr) {
    // 設定每次冒泡的終止點
    for (int i = arr.length-1; i > 0; i--) {
        boolean change = false;
        // 從起點開始冒泡
        for (int j = 0; j < i; j++) {
            if (arr[j] > arr[j+1]) {
                int temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
                if (change == false) {
                    change = true;
                }
            }
        }
        // 如果本次無交換,則表示已有序,排序完成
        if (!change) {
            return;
        }
    }
}

 


插入排序

演算法思想

  通過構建有序序列,對於未排序資料,在已排序序列中從後向前掃描,找到相應位置並插入。每步將一個待排序的元素,按其排序碼大小插入到前面已經排好序的一組元素的適當位置上去,直到元素全部插入為止。

  演算法原理參考:圖解插入排序

程式碼實現

// 插入排序
public static void insertionSort(int[] arr) {
    // 從第二個元素開始為它們找位置
    for (int i = 1; i < arr.length; i++) {
        // 記住當前元素
        int temp = arr[i];
        int j;
        // 從當前元素左邊第一個元素開始,向左找位置
        for (j = i-1; j >= 0 && arr[j] > temp; j--) {
            arr[j+1] = arr[j];
        }
        // 找到合適位置後,將當前元素插入
        arr[j+1] = temp;
    }
}

 


希爾排序

演算法思想

  又稱“分組插入排序”,先將整個待排元素序列分割成若干個子序列(由相隔某個“增量”的元素組成的)分別進行直接插入排序,然後依次縮減增量再進行排序,待整個序列中的元素基本有序(增量足夠小)時,再對全體元素進行一次直接插入排序。

  演算法原理參考:圖解希爾排序

  這個排序演算法較難理解,建議讀者在本子上依照程式碼的執行過程,把排序過程動手畫一遍,這應該是理解其本質最快的方法了。

程式碼實現

// 希爾排序
public static void shellSort(int[] arr) {
    // 增量控制,每次減半
    for (int gap = arr.length/2; gap > 0; gap /= 2) {
        // 步長控制,從gap開始向後移動
        for (int i = gap; i < arr.length; i++) {
            int temp = arr[i];
            int j;
            // 起始指標控制,向左插入排序(找位置)
            for (j = i-gap; j >= 0 && arr[j] > temp; j -= gap) {
                arr[j+gap] = arr[j];
            }
            arr[j+gap] = temp;
        }
    }
}

 


歸併排序

演算法思想

  該演算法採用經典的分治(divide-and-conquer)策略,分:問題分成一些小的問題然後遞迴求解,治:將分的階段得到的各答案"修補"在一起,即分而治之。將序列遞迴拆半分成多個子序列,再將各個子序列排序後歸併疊加,最後歸併所有子序列,排序完成。

  演算法原理參考:圖解歸併排序

程式碼實現

// 歸併排序
public static void mergeSort(int[] arr, int left, int right) {
    if (left < right) {
        int mid = (left + right) / 2;
        //
        mergeSort(arr, left, mid);
        mergeSort(arr, mid+1, right);
        //
        merge(arr, left, mid, right);
    }
}

public static void merge(int[] arr, int left, int mid, int right) {
    // 臨時陣列
    int[] temp = new int[right-left+1];
    // 左、右指標
    int i = left;
    int j = mid+1;
    int k = 0;
    // 將兩段資料按序寫入臨時陣列
    while (i <= mid && j <= right) {
        if (arr[i] <= arr[j]) {
            temp[k++] = arr[i++];
        }else {
            temp[k++] = arr[j++];
        }
    }
    // 剩餘資料寫入
    while (i <= mid) {
        temp[k++] = arr[i++];
    }
    while (j <= right) {
        temp[k++] = arr[j++];
    }
    // 更新arr陣列
    k = 0;
    while (left <= right) {
        arr[left++] = temp[k++];
    }
}

 


快速排序

演算法思想

  以第一個元素為基準,左右指標向中間移動掃描,小於基準元素的放到左邊,大於基準元素的放到右邊,最後將基準元素放到中間,這個位置也就是基準元素的合適位置。此時資料以基準元素為間隔,分為左右兩部分(不包括基準元素),然後分別對左右兩部分資料分別執行此過程,直到資料不能再分,此時排序完成。

  演算法原理參考:圖解快速排序

程式碼實現

// 快速排序
public static void quickSort(int[] arr, int left, int right) {
    if (left < right) {
        int index = getIndex(arr, left, right);
        quickSort(arr, left, index-1);
        quickSort(arr, index+1, right);
    }
}

public static int getIndex(int[] arr, int left, int right) {
    // 得到基準元素
    int item = arr[left];
    // 向中間移動,調整位置,找到基準元素的位置
    while (left < right) {
        while (left < right && arr[right] >= item) {
            right--;
        }
        arr[left] = arr[right];
        while (left < right && arr[left] <= item) {
            left++;
        }
        arr[right] = arr[left];
    }
    // 放置基準元素
    arr[left] = item;
    return left;
}

 


堆排序

演算法思想

  將待排序序列構造成一個大頂堆,此時,整個序列的最大值就是堆頂的根節點。將其與末尾元素進行交換,此時末尾就為最大值。然後將剩餘n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值。如此反覆執行,便能得到一個有序序列了。

  演算法原理參考:圖解堆排序

程式碼實現

// 堆排序
public static void heapSort(int[] arr) {
    // 1.構建大頂堆
    for (int i = arr.length/2-1; i >= 0; i--) {
        adjust(arr, i, arr.length);
    }
    // 2.調整堆結構,交換頂元素與末尾元素
    for (int j = arr.length-1 ; j > 0; j--) {
        int temp = arr[j];
        arr[j] = arr[0];
        arr[0] = temp;
        adjust(arr, 0, j);
    }
}

public static void adjust(int[] arr, int i, int length) {
    int temp = arr[i];
    for (int k = 2*i+1; k < length; k = 2*k+1) {
        if (k+1 < length && arr[k+1] > arr[k]) {
            k++;
        }
        if (arr[k] > temp) {
            arr[i] = arr[k];
            i = k;
        }else {
            break;
        }
    }
    arr[i] = temp;
}

 


計數排序

演算法思想

  計數排序要求輸入的資料必須是有確定範圍的整數。

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

  演算法原理參考:圖解計數排序

程式碼實現

// 計數排序
public static int[] countSort(int[] src) {
    // 計算最大值與最小值
    int max = Integer.MIN_VALUE;
    int min = Integer.MAX_VALUE;
    for(int i = 0; i < src.length; i++){
        max = Math.max(max, src[i]);
        min = Math.min(min, src[i]);
    }

    // 陣列中的資料範圍
    int range = max - min + 1;
    
    // count[i]表示陣列src中資料min + i的個數
    int[] count = new int[range];
    for (int value : src) {
        count[value - min]++;
    }

    // 累計變形,使得count[i]儲存src中小於等於min + i的資料的個數
    for (int i = 1; i < range; i++) {
        count[i] += count[i - 1];
    }

    // 結果陣列
    int[] res = new int[src.length];
    
    // 倒序遍歷原陣列,保持排序的穩定性
    for (int i = src.length - 1; i >= 0; i--) {
        // 獲取資料在count陣列中的索引
        int index = src[i] - min;
        // 個數減1
        count[index]--;
        // 資料src[i]排序後的索引是count[index]
        res[count[index]] = src[i];
    }
    return res;
}

 


桶排序

演算法思想

  桶排序是計數排序的擴充套件版本,計數排序可以看成每個桶只儲存相同元素,而桶排序每個桶儲存一定範圍的元素,通過對映函式,將待排序陣列中的元素對映到各個對應的桶中,對每個桶中的元素進行排序,最後將非空桶中的元素逐個放入原序列中。

  桶排序需要儘量保證元素分散均勻,否則當所有資料集中在同一個桶中時,桶排序失效。

  演算法原理參考:圖解桶排序

程式碼實現

// 桶排序
public static void bucketSort(int[] arr){
    
    // 計算最大值與最小值
    int max = Integer.MIN_VALUE;
    int min = Integer.MAX_VALUE;
    for(int i = 0; i < arr.length; i++){
        max = Math.max(max, arr[i]);
        min = Math.min(min, arr[i]);
    }
    
    // 計算桶的數量,並建立桶
    int bucketNum = (max - min) / arr.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 < arr.length; i++){
        int num = (arr[i] - min) / (arr.length);
        bucketArr.get(num).add(arr[i]);
    }
    
    // 對每個桶進行排序
    for(int i = 0; i < bucketArr.size(); i++){
        Collections.sort(bucketArr.get(i));
    }
    
    // 將桶中的元素賦值到原序列
    int index = 0;
    for(int i = 0; i < bucketArr.size(); i++){
        for(int j = 0; j < bucketArr.get(i).size(); j++){
            arr[index++] = bucketArr.get(i).get(j);
        }
    }  
}

 


基數排序

演算法思想

  基數排序可以看成是桶排序的擴充套件,以整數排序為例,主要思想是將整數按位數劃分,準備 10 個桶,代表 0 - 9,根據整數個位數字的數值將元素放入對應的桶中,之後按照輸入賦值到原序列中,依次對十位、百位等進行同樣的操作,最終就完成了排序的操作。

  演算法原理參考:圖解基數排序

程式碼實現

public static void radixSort(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    radixSort(arr, 0, arr.length - 1, maxbits(arr));
}
    
// 計算最大位數
public static int maxbits(int[] arr) {
    int max = Integer.MIN_VALUE;
    for (int i = 0; i < arr.length; i++) {
        max = Math.max(max, arr[i]);
    }
    int res = 0;
    while (max != 0) {
        res++;
        max /= 10;
    }
    return res;
}

// 基數排序
public static void radixSort(int[] arr, int begin, int end, int digit) {
    final int radix = 10;
    int i = 0, j = 0;
    int[] count = new int[radix];
    int[] bucket = new int[end - begin + 1];
    // 依次遍歷每個位數
    for (int d = 1; d <= digit; d++) {
        for (i = 0; i < radix; i++) {
            count[i] = 0;
        }
            
        // 統計數量
        for (i = begin; i <= end; i++) {
            j = getDigit(arr[i], d);
            count[j]++;
        }
            
        // 計算位置
        for (i = 1; i < radix; i++) {
            count[i] = count[i] + count[i - 1];
        }
            
        // 記錄到對應位置
        for (i = end; i >= begin; i--) {
            j = getDigit(arr[i], d);
            count[j]--;
            bucket[count[j]] = arr[i];
        }
        for (i = begin, j = 0; i <= end; i++, j++) {
            arr[i] = bucket[j];
        }
    }
}

// 獲取位數數值
public static int getDigit(int x, int d) {
    return ((x / ((int) Math.pow(10, d - 1))) % 10);
}

 


排序演算法梳理

複雜度分析

演算法分類及適用場景

  ★ 選擇、冒泡、插入、希爾、歸併、快速、堆排序都是基於比較的排序

    ➷ 平均時間複雜度最低O(nlogn)。

    ➷ 適用於所有可比較的物件。

  ★ 計數排序、桶排序、基數排序不是基於比較的排序

    ➷ 使用空間換時間,某些時候,平均時間複雜度可以低於O(nlogn)。

    ➷ 適用於正整數的比較。

穩定性分析

  ✔ 不穩定的排序演算法有:快速排序、希爾排序、選擇排序、堆排序。

    ✄ 巧記:快『快速排序』、些『希爾排序』、選『選擇排序』、堆『堆排序』。

  ✔ 穩定的排序演算法有:氣泡排序、插入排序、歸併排序、計數排序、桶排序、基數排序。

相關文章