八種排序算法能夠按照如圖分類java
所謂交換,就是序列中任意兩個元素進行比較,根據比較結果來交換各自在序列中的位置,以此達到排序的目的。算法
冒泡排序是一種簡單的交換排序算法,以升序排序爲例,其核心思想是:shell
用 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; } } } }
如今有個問題,假如待排序數組是 二、一、三、四、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; }
快速排序的思想很簡單,就是先把待排序的數組拆成左右兩個區間,左邊都比中間的基準數小,右邊都比基準數大。接着左右兩邊各自再作一樣的操做,完成後再拆分再繼續,一直到各區間只有一個數爲止。指針
舉個例子,如今我要排序 四、九、五、一、二、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); } }
插入排序是一種簡單的排序方法,其基本思想是將一個記錄插入到已經排好序的有序表中,使得被插入數的序列一樣是有序的。按照此法對全部元素進行插入,直到整個序列排爲有序的過程。
直接插入排序就是插入排序的粗暴實現。對於一個序列,選定一個下標,認爲在這個下標以前的元素都是有序的。將下標所在的元素插入到其以前的序列中。接着再選取這個下標的後一個元素,繼續重複操做。直到最後一個元素完成插入爲止。咱們通常從序列的第二個元素開始操做。
用 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; } } }
某些狀況下直接插入排序的效率極低。好比一個已經有序的升序數組,這時再插入一個比最小值還要小的數,也就意味着被插入的數要和數組全部元素比較一次。咱們須要對直接插入排序進行改進。
怎麼改進呢?前面提過,插入排序對已經排好序的數組操做時,效率很高。所以咱們能夠試着先將數組變爲一個相對有序的數組,而後再作插入排序。
希爾排序能實現這個目的。希爾排序把序列按下標的必定增量(步長)分組,對每組分別使用插入排序。隨着增量(步長)減小,一直到一,算法結束,整個序列變爲有序。所以希爾排序又稱縮小增量排序。
通常來講,初次取序列的一半爲增量,之後每次減半,直到增量爲一。
用 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; } } } } }
選擇排序是一種簡單直觀的排序算法,首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,而後,再從剩餘未排序元素中繼續尋找最小(大)元素,而後放到已排序序列的末尾。以此類推,直到全部元素均排序完畢。
選擇排序思想的暴力實現,每一趟從未排序的區間找到一個最小元素,並放到第一位,直到所有區間有序爲止。
用 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; } } }
咱們知道,對於任何一個數組均可以當作一顆徹底二叉樹。堆是具備如下性質的徹底二叉樹:每一個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆;或者每一個結點的值都小於或等於其左右孩子結點的值,稱爲小頂堆。以下圖:
像上圖的大頂堆,映射爲數組,就是 [50, 45, 40, 20, 25, 35, 30, 10, 15]。能夠發現第一個下標的元素就是最大值,將其與末尾元素交換,則末尾元素就是最大值。因此堆排序的思想能夠概括爲如下兩步:
重複以上兩個步驟,直到沒有元素可操做,就完成排序了。
咱們須要把一個普通數組轉換爲大頂堆,調整的起始點是最後一個非葉子結點,而後從左至右,從下至上,繼續調整其餘非葉子結點,直到根結點爲止。
/** * 轉化爲大頂堆 * @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); } }
歸併排序是創建在歸併操做上的一種有效,穩定的排序算法。該算法採用分治法的思想,是一個很是典型的應用。歸併排序的思路以下:
關鍵在於兩個子序列應該如何合併。假設兩個子序列各自都是有序的,那麼合併步驟就是:
用 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) |