八種排序演算法可以按照如圖分類
交換排序
所謂交換,就是序列中任意兩個元素進行比較,根據比較結果來交換各自在序列中的位置,以此達到排序的目的。
1. 氣泡排序
氣泡排序是一種簡單的交換排序演算法,以升序排序為例,其核心思想是:
- 從第一個元素開始,比較相鄰的兩個元素。如果第一個比第二個大,則進行交換。
- 輪到下一組相鄰元素,執行同樣的比較操作,再找下一組,直到沒有相鄰元素可比較為止,此時最後的元素應是最大的數。
- 除了每次排序得到的最後一個元素,對剩餘元素重複以上步驟,直到沒有任何一對元素需要比較為止。
用 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
為什麼把快排劃到交換排序的範疇呢?因為元素的移動也是靠交換位置來實現的。演算法的實現需要用到遞迴(拆分割槽間之後再對每個區間作同樣的操作)
用 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]。可以發現第一個下標的元素就是最大值,將其與末尾元素交換,則末尾元素就是最大值。所以堆排序的思想可以歸納為以下兩步:
- 根據初始陣列構造堆
- 每次交換第一個和最後一個元素,然後將除最後一個元素以外的其他元素重新調整為大頂堆
重複以上兩個步驟,直到沒有元素可操作,就完成排序了。
我們需要把一個普通陣列轉換為大頂堆,調整的起始點是最後一個非葉子結點,然後從左至右,從下至上,繼續調整其他非葉子結點,直到根結點為止。
/**
* 轉化為大頂堆
* @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);
}
}
歸併排序
歸併排序是建立在歸併操作上的一種有效,穩定的排序演算法。該演算法採用分治法的思想,是一個非常典型的應用。歸併排序的思路如下:
- 將 n 個元素分成兩個各含 n/2 個元素的子序列
- 藉助遞迴,兩個子序列分別繼續進行第一步操作,直到不可再分為止
- 此時每一層遞迴都有兩個子序列,再將其合併,作為一個有序的子序列返回上一層,再繼續合併,全部完成之後得到的就是一個有序的序列
關鍵在於兩個子序列應該如何合併。假設兩個子序列各自都是有序的,那麼合併步驟就是:
- 建立一個用於存放結果的臨時陣列,其長度是兩個子序列合併後的長度
- 設定兩個指標,最初位置分別為兩個已經排序序列的起始位置
- 比較兩個指標所指向的元素,選擇相對小的元素放入臨時陣列,並移動指標到下一位置
- 重複步驟 3 直到某一指標達到序列尾
- 將另一序列剩下的所有元素直接複製到合併序列尾
用 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(nlogn) |
歸併排序 | O(nlogn) | O(nlogn) | O(nlogn) | 穩定 | O(n) |