用 Java 實現的八種經常使用排序算法


八種排序算法能夠按照如圖分類java


交換排序

所謂交換,就是序列中任意兩個元素進行比較,根據比較結果來交換各自在序列中的位置,以此達到排序的目的。算法

1. 冒泡排序

冒泡排序是一種簡單的交換排序算法,以升序排序爲例,其核心思想是:shell

  1. 從第一個元素開始,比較相鄰的兩個元素。若是第一個比第二個大,則進行交換。
  2. 輪到下一組相鄰元素,執行一樣的比較操做,再找下一組,直到沒有相鄰元素可比較爲止,此時最後的元素應是最大的數。
  3. 除了每次排序獲得的最後一個元素,對剩餘元素重複以上步驟,直到沒有任何一對元素須要比較爲止。

用 Java 實現的冒泡排序以下數組

public void bubbleSortOpt(int[] arr) {

    if(arr == null) {
        throw new NullPoniterException();
    }
    if(arr.length < 2) {
        return;
    }
    int temp = 0;
    for(int i = 0; i < arr.length - 1; i++) {
        for(int j = 0; j < arr.length - i - 1; j++) {
            if(arr[j] > arr[j + 1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

2. 冒泡排序優化

如今有個問題,假如待排序數組是 二、一、三、四、5 這樣的狀況,按照上述代碼實現,第一次循環便可得出正確結果。但循環並不會中止,而是繼續執行,直到結束爲止。顯然,以後的循環遍歷是沒有必要的。優化

爲了解決這個問題,咱們能夠設置一個標誌位,用來表示當前次循環是否有交換,若是沒有,則說明當前數組已經徹底排序。ui

public static int bubbleSortOpt2(int[] arr) {

    if (arr == null) {
        throw new NullPointerException();
    } else if (arr.length < 2) {
        return 0;
    }

    int temp;
    int count = 0;
    for (int i = 0; i < arr.length - 1; i++) {
        int flag = 1;
        for (int j = 0; j < arr.length - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                flag = 0;
            }
            count++;
        }
        // 沒有發生交換,排序已經完成
        if (flag == 1) {
            return count;
        }
    }
    return count;
}

算法還能夠再優化,好比 三、四、二、一、六、七、8 這個數組,第一次循環後,變爲 三、二、一、四、六、七、8 的順序,咱們發現,1 以後的 4 、六、七、8 已經有序了,第二次循環就不必對後面這段再遍歷比較。spa

假設一次循環後數組第 i 個元素後全部元素已經有序,優化目標就是下次循環再也不花費時間遍歷已經有序的部分。關鍵在於如何定位 i 這個分界點,其實並不難,能夠想象,因爲 i 以前的元素是無序的,因此必定有交換髮生,而 i 以後的元素已經有序,不會發生交換,最後發生交換的地點,就是咱們要找的分界點。3d

public static int bubbleSortOpt3(int[] arr) {

    if (arr == null) {
        throw new RuntimeException();
    } else if (arr.length < 2) {
        return 0;
    }

    int temp;
    int count = 0;
    int len = arr.length - 1;
    for (int i = 0; i < len; i++) {
        // 記錄最後一次交換位置
        int lastChange = 0;
        for (int j = 0; j < len; j++) {
            if (arr[j] > arr[j + 1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                // 每交換一次更新一次
                lastChange = j;
            }
            count++;
        }
        // 沒有發生交換,排序已經完成
        if (lastChange == 0) {
            return count;
        }
        len = lastChange;
    }
    return count;
}

3. 快速排序

快速排序的思想很簡單,就是先把待排序的數組拆成左右兩個區間,左邊都比中間的基準數小,右邊都比基準數大。接着左右兩邊各自再作一樣的操做,完成後再拆分再繼續,一直到各區間只有一個數爲止。指針

舉個例子,如今我要排序 四、九、五、一、二、6 這個數組。通常取首位的 4 爲基準數,第一次排序的結果是:code

二、一、四、五、九、6

可能有人以爲奇怪,2 和 1 交換下位置也能知足條件,爲何 2 在首位?這其實由實際的代碼實現來決定,並不影響以後的操做。以 4 爲分界點,對 二、一、4 和 五、九、6 各自排序,獲得:

一、二、四、五、九、6

不用管左邊的 一、二、4 了,將 五、九、6 拆成 5 和 九、6,再排序,至此結果爲:

一、二、四、五、六、9

爲何把快排劃到交換排序的範疇呢?由於元素的移動也是靠交換位置來實現的。算法的實現須要用到遞歸(拆分區間以後再對每一個區間做一樣的操做)

用 Java 實現的快速排序以下

public void quicksort(int[] arr, int start, int end) {

    if(start < end) {
        // 把數組中的首位數字做爲基準數
        int stard = arr[start];
        // 記錄須要排序的下標
        int low = start;
        int high = end;
        // 循環找到比基準數大的數和比基準數小的數
        while(low < high) {
            // 右邊的數字比基準數大
            while(low < high && arr[high] >= stard) {
                high--;
            }
            // 使用右邊的數替換左邊的數
            arr[low] = arr[high];
            // 左邊的數字比基準數小
            while(low < high && arr[low] <= stard) {
                low++;
            }
            // 使用左邊的數替換右邊的數
            arr[high] = arr[low];
        }
        // 把標準值賦給下標重合的位置
        arr[low] = stard;
        // 處理全部小的數字
        quickSort(arr, start, low);
        // 處理全部大的數字
        quickSort(arr, low + 1, end);
    }
}

插入排序

插入排序是一種簡單的排序方法,其基本思想是將一個記錄插入到已經排好序的有序表中,使得被插入數的序列一樣是有序的。按照此法對全部元素進行插入,直到整個序列排爲有序的過程。

1. 直接插入排序

直接插入排序就是插入排序的粗暴實現。對於一個序列,選定一個下標,認爲在這個下標以前的元素都是有序的。將下標所在的元素插入到其以前的序列中。接着再選取這個下標的後一個元素,繼續重複操做。直到最後一個元素完成插入爲止。咱們通常從序列的第二個元素開始操做。

用 Java 實現的算法以下:

public void insertSort(int[] arr) {
    // 遍歷全部數字
    for(int i = 1; i < arr.length - 1; i++) {
        // 當前數字比前一個數字小
        if(arr[i] < arr[i - 1]) {
            int j;
            // 把當前遍歷的數字保存起來
            int temp = arr[i];
            for(j = i - 1; j >= 0 && arr[j] > temp; j--) {
                // 前一個數字賦給後一個數字
                arr[j + 1] = arr[j];
            }
            // 把臨時變量賦給不知足條件的後一個元素
            arr[j + 1] = temp;
        }
    }
}

2. 希爾排序

某些狀況下直接插入排序的效率極低。好比一個已經有序的升序數組,這時再插入一個比最小值還要小的數,也就意味着被插入的數要和數組全部元素比較一次。咱們須要對直接插入排序進行改進。

怎麼改進呢?前面提過,插入排序對已經排好序的數組操做時,效率很高。所以咱們能夠試着先將數組變爲一個相對有序的數組,而後再作插入排序。

希爾排序能實現這個目的。希爾排序把序列按下標的必定增量(步長)分組,對每組分別使用插入排序。隨着增量(步長)減小,一直到一,算法結束,整個序列變爲有序。所以希爾排序又稱縮小增量排序。

通常來講,初次取序列的一半爲增量,之後每次減半,直到增量爲一。

用 Java 實現的算法以下:

public void shellSort(int[] arr) {
    // gap 爲步長,每次減爲原來的一半。
    for (int gap = arr.length / 2; gap > 0; gap /= 2) {
        // 對每一組都執行直接插入排序
        for (int i = 0 ;i < gap; i++) {
            // 對本組數據執行直接插入排序
            for (int j = i + gap; j < arr.length; j += gap) {
                // 若是 a[j] < a[j-gap],則尋找 a[j] 位置,並將後面數據的位置都後移
                if (arr[j] < arr[j - gap]) {
                    int k;
                    int temp = arr[j];
                    for (k = j - gap; k >= 0 && arr[k] > temp; k -= gap) {
                        arr[k + gap] = arr[k];
                    }
                    arr[k + gap] = temp;
                }
            }
        }
    }
}

選擇排序

選擇排序是一種簡單直觀的排序算法,首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,而後,再從剩餘未排序元素中繼續尋找最小(大)元素,而後放到已排序序列的末尾。以此類推,直到全部元素均排序完畢。

1. 簡單選擇排序

選擇排序思想的暴力實現,每一趟從未排序的區間找到一個最小元素,並放到第一位,直到所有區間有序爲止。

用 Java 實現的算法以下:

public void selectSort(int[] arr) {
    // 遍歷全部的數
    for (int i = 0; i < arr.length; i++) {
        int minIndex = i;
        // 把當前遍歷的數和後面全部的數進行比較,並記錄下最小的數的下標
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[j] < arr[minIndex]) {
                // 記錄最小的數的下標
                minIndex = j;
            }
        }
        // 若是最小的數和當前遍歷的下標不一致,則交換
        if (i != minIndex) {
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
}

2. 堆排序

咱們知道,對於任何一個數組均可以當作一顆徹底二叉樹。堆是具備如下性質的徹底二叉樹:每一個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆;或者每一個結點的值都小於或等於其左右孩子結點的值,稱爲小頂堆。以下圖:

像上圖的大頂堆,映射爲數組,就是 [50, 45, 40, 20, 25, 35, 30, 10, 15]。能夠發現第一個下標的元素就是最大值,將其與末尾元素交換,則末尾元素就是最大值。因此堆排序的思想能夠概括爲如下兩步:

  1. 根據初始數組構造堆
  2. 每次交換第一個和最後一個元素,而後將除最後一個元素之外的其餘元素從新調整爲大頂堆

重複以上兩個步驟,直到沒有元素可操做,就完成排序了。

咱們須要把一個普通數組轉換爲大頂堆,調整的起始點是最後一個非葉子結點,而後從左至右,從下至上,繼續調整其餘非葉子結點,直到根結點爲止。

/**
 * 轉化爲大頂堆
 * @param arr 待轉化的數組
 * @param size 待調整的區間長度
 * @param index 結點下標
 */
public void maxHeap(int[] arr, int size, int index) {
    // 左子結點
    int leftNode = 2 * index + 1;
    // 右子結點
    int rightNode = 2 * index + 2;
    int max = index;
    // 和兩個子結點分別對比,找出最大的結點
    if (leftNode < size && arr[leftNode] > arr[max]) {
        max = leftNode;
    }
    if (rightNode < size && arr[rightNode] > arr[max]) {
        max = rightNode;
    }
    // 交換位置
    if (max != index) {
        int temp = arr[index];
        arr[index] = arr[max];
        arr[max] = temp;
        // 由於交換位置後有可能使子樹不知足大頂堆條件,因此要對子樹進行調整
        maxHeap(arr, size, max);
    }
}

/**
 * 堆排序
 * @param arr 待排序的整型數組
 */
public static void heapSort(int[] arr) {
    // 開始位置是最後一個非葉子結點,即最後一個結點的父結點
    int start = (arr.length - 1) / 2;
    // 調整爲大頂堆
    for (int i = start; i >= 0; i--) {
        SortTools.maxHeap(arr, arr.length, i);
    }
    // 先把數組中第 0 個位置的數和堆中最後一個數交換位置,再把前面的處理爲大頂堆
    for (int i = arr.length - 1; i > 0; i--) {
        int temp = arr[0];
        arr[0] = arr[i];
        arr[i] = temp;
        maxHeap(arr, i, 0);
    }
}

歸併排序

歸併排序是創建在歸併操做上的一種有效,穩定的排序算法。該算法採用分治法的思想,是一個很是典型的應用。歸併排序的思路以下:

  1. 將 n 個元素分紅兩個各含 n/2 個元素的子序列
  2. 藉助遞歸,兩個子序列分別繼續進行第一步操做,直到不可再分爲止
  3. 此時每一層遞歸都有兩個子序列,再將其合併,做爲一個有序的子序列返回上一層,再繼續合併,所有完成以後獲得的就是一個有序的序列

關鍵在於兩個子序列應該如何合併。假設兩個子序列各自都是有序的,那麼合併步驟就是:

  1. 建立一個用於存放結果的臨時數組,其長度是兩個子序列合併後的長度
  2. 設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置
  3. 比較兩個指針所指向的元素,選擇相對小的元素放入臨時數組,並移動指針到下一位置
  4. 重複步驟 3 直到某一指針達到序列尾
  5. 將另外一序列剩下的全部元素直接複製到合併序列尾

用 Java 實現的歸併排序以下:

/**
 * 合併數組
 */
public static void merge(int[] arr, int low, int middle, int high) {
    // 用於存儲歸併後的臨時數組
    int[] temp = new int[high - low + 1];
    // 記錄第一個數組中須要遍歷的下標
    int i = low;
    // 記錄第二個數組中須要遍歷的下標
    int j = middle + 1;
    // 記錄在臨時數組中存放的下標
    int index = 0;
    // 遍歷兩個數組,取出小的數字,放入臨時數組中
    while (i <= middle && j <= high) {
        // 第一個數組的數據更小
        if (arr[i] <= arr[j]) {
            // 把更小的數據放入臨時數組中
            temp[index] = arr[i];
            // 下標向後移動一位
            i++;
        } else {
            temp[index] = arr[j];
            j++;
        }
        index++;
    }
    // 處理剩餘未比較的數據
    while (i <= middle) {
        temp[index] = arr[i];
        i++;
        index++;
    }
    while (j <= high) {
        temp[index] = arr[j];
        j++;
        index++;
    }
    // 把臨時數組中的數據從新放入原數組
    for (int k = 0; k < temp.length; k++) {
        arr[k + low] = temp[k];
    }
}

/**
 * 歸併排序
 */
public static void mergeSort(int[] arr, int low, int high) {
    int middle = (high + low) / 2;
    if (low < high) {
        // 處理左邊數組
        mergeSort(arr, low, middle);
        // 處理右邊數組
        mergeSort(arr, middle + 1, high);
        // 歸併
        merge(arr, low, middle, high);
    }
}

基數排序

基數排序的原理是將整數按位數切割成不一樣的數字,而後按每一個位數分別比較。爲此須要將全部待比較的數值統一爲一樣的數位長度,數位不足的數在高位補零。

使用 Java 實現的基數排序:

/**
 * 基數排序
 */
public static void radixSort(int[] arr) {
    // 存放數組中的最大數字
    int max = Integer.MIN_VALUE;
    for (int value : arr) {
        if (value > max) {
            max = value;
        }
    }
    // 計算最大數字是幾位數
    int maxLength = (max + "").length();
    // 用於臨時存儲數據
    int[][] temp = new int[10][arr.length];
    // 用於記錄在 temp 中相應的下標存放數字的數量
    int[] counts = new int[10];
    // 根據最大長度的數決定比較次數
    for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
        // 每個數字分別計算餘數
        for (int j = 0; j < arr.length; j++) {
            // 計算餘數
            int remainder = arr[j] / n % 10;
            // 把當前遍歷的數據放到指定的數組中
            temp[remainder][counts[remainder]] = arr[j];
            // 記錄數量
            counts[remainder]++;
        }
        // 記錄取的元素須要放的位置
        int index = 0;
        // 把數字取出來
        for (int k = 0; k < counts.length; k++) {
            // 記錄數量的數組中當前餘數記錄的數量不爲 0
            if (counts[k] != 0) {
                // 循環取出元素
                for (int l = 0; l < counts[k]; l++) {
                    arr[index] = temp[k][l];
                    // 記錄下一個位置
                    index++;
                }
                // 把數量置空
                counts[k] = 0;
            }
        }
    }
}

八種排序算法的總結

排序法 最好情形 平均時間 最差情形 穩定度 空間複雜度
冒泡排序 O(n) O(n^2^) O(n^2^) 穩定 O(1)
快速排序 O(nlogn) O(nlogn) O(n^2^) 不穩定 O(nlogn)
直接插入排序 O(n) O(n^2^) O(n^2^) 穩定 O(1)
希爾排序 不穩定 O(1)
直接選擇排序 O(n^2^) O(n^2^) O(n^2^) 不穩定 O(1)
堆排序 O(nlogn) O(nlogn) O(nlogn) 不穩定 O(1)
歸併排序 O(nlogn) O(nlogn) O(nlogn) 穩定 O(n + logn)
相關文章
相關標籤/搜索