1. 程式人生 > >用 Java 實現的八種常用排序演算法

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


> 八種排序演算法可以按照如圖分類 ![](https://img2020.cnblogs.com/blog/1759254/202010/1759254-20201013141619825-490940992.png)
## 交換排序 所謂交換,就是序列中任意兩個元素進行比較,根據比較結果來交換各自在序列中的位置,以此達到排序的目的。 #### 1. 氣泡排序 氣泡排序是一種簡單的交換排序演算法,以升序排序為例,其核心思想是: 1. 從第一個元素開始,比較相鄰的兩個元素。如果第一個比第二個大,則進行交換。 2. 輪到下一組相鄰元素,執行同樣的比較操作,再找下一組,直到沒有相鄰元素可比較為止,此時最後的元素應是最大的數。 3. 除了每次排序得到的最後一個元素,對剩餘元素重複以上步驟,直到沒有任何一對元素需要比較為止。 ![](https://img2020.cnblogs.com/blog/1759254/202010/1759254-20201013154401506-1553295050.gif) 用 Java 實現的氣泡排序如下 ```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. 快速排序 快速排序的思想很簡單,就是先把待排序的陣列拆成左右兩個區間,左邊都比中間的基準數小,右邊都比基準數大。接著左右兩邊各自再做同樣的操作,完成後再拆分再繼續,一直到各區間只有一個數為止。 舉個例子,現在我要排序 4、9、5、1、2、6 這個陣列。一般取首位的 4 為基準數,第一次排序的結果是: 2、1、4、5、9、6 可能有人覺得奇怪,2 和 1 交換下位置也能滿足條件,為什麼 2 在首位?這其實由實際的程式碼實現來決定,並不影響之後的操作。以 4 為分界點,對 2、1、4 和 5、9、6 各自排序,得到: 1、2、4、5、9、6 不用管左邊的 1、2、4 了,將 5、9、6 拆成 5 和 9、6,再排序,至此結果為: 1、2、4、5、6、9 為什麼把快排劃到交換排序的範疇呢?因為元素的移動也是靠交換位置來實現的。演算法的實現需要用到遞迴(拆分割槽間之後再對每個區間作同樣的操作) ![](https://img2020.cnblogs.com/blog/1759254/202010/1759254-20201013154431154-241610869.gif) 用 Java 實現的快速排序如下 ```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. 直接插入排序 直接插入排序就是插入排序的粗暴實現。對於一個序列,選定一個下標,認為在這個下標之前的元素都是有序的。將下標所在的元素插入到其之前的序列中。接著再選取這個下標的後一個元素,繼續重複操作。直到最後一個元素完成插入為止。我們一般從序列的第二個元素開始操作。 ![](https://img2020.cnblogs.com/blog/1759254/202010/1759254-20201013154456350-100297711.gif) ![](https://img2020.cnblogs.com/blog/1759254/202010/1759254-20201013154504717-901504156.jpg) 用 Java 實現的演算法如下: ```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. 希爾排序 某些情況下直接插入排序的效率極低。比如一個已經有序的升序陣列,這時再插入一個比最小值還要小的數,也就意味著被插入的數要和陣列所有元素比較一次。我們需要對直接插入排序進行改進。 怎麼改進呢?前面提過,插入排序對已經排好序的陣列操作時,效率很高。因此我們可以試著先將陣列變為一個相對有序的陣列,然後再做插入排序。 希爾排序能實現這個目的。希爾排序把序列按下標的一定增量(步長)分組,對每組分別使用插入排序。隨著增量(步長)減少,一直到一,演算法結束,整個序列變為有序。因此希爾排序又稱縮小增量排序。 一般來說,初次取序列的一半為增量,以後每次減半,直到增量為一。 ![](https://img2020.cnblogs.com/blog/1759254/202010/1759254-20201013154523252-1638498584.jpg) 用 Java 實現的演算法如下: ```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. 簡單選擇排序 選擇排序思想的暴力實現,每一趟從未排序的區間找到一個最小元素,並放到第一位,直到全部區間有序為止。 ![](https://img2020.cnblogs.com/blog/1759254/202010/1759254-20201013154544584-329827452.gif) 用 Java 實現的演算法如下: ```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. 堆排序 我們知道,對於任何一個數組都可以看成一顆完全二叉樹。堆是具有以下性質的完全二叉樹:每個結點的值都大於或等於其左右孩子結點的值,稱為大頂堆;或者每個結點的值都小於或等於其左右孩子結點的值,稱為小頂堆。如下圖: ![](https://img2020.cnblogs.com/blog/1759254/202010/1759254-20201013154619424-968167627.png) 像上圖的大頂堆,對映為陣列,就是 [50, 45, 40, 20, 25, 35, 30, 10, 15]。可以發現第一個下標的元素就是最大值,將其與末尾元素交換,則末尾元素就是最大值。所以堆排序的思想可以歸納為以下兩步: 1. 根據初始陣列構造堆 2. 每次交換第一個和最後一個元素,然後將除最後一個元素以外的其他元素重新調整為大頂堆 重複以上兩個步驟,直到沒有元素可操作,就完成排序了。 我們需要把一個普通陣列轉換為大頂堆,調整的起始點是最後一個非葉子結點,然後從左至右,從下至上,繼續調整其他非葉子結點,直到根結點為止。 ![](https://img2020.cnblogs.com/blog/1759254/202010/1759254-20201013154634251-1764142328.gif) ```java /** * 轉化為大頂堆 * @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. 將另一序列剩下的所有元素直接複製到合併序列尾 ![](https://img2020.cnblogs.com/blog/1759254/202010/1759254-20201013155743335-1872802731.gif) 用 Java 實現的歸併排序如下: ```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); } } ```
## 基數排序 基數排序的原理是將整數按位數切割成不同的數字,然後按每個位數分別比較。為此需要將所有待比較的數值統一為同樣的數位長度,數位不足的數在高位補零。 ![](https://img2020.cnblogs.com/blog/1759254/202010/1759254-20201013160254262-965480580.gif) 使用 Java 實現的基數排序: ```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(nlogn)
歸併排序 O(nlogn) O(nlogn) O(nlogn) 穩定 O(n)