排序演算法(一)時間複雜度為O(n²)的排序演算法
排序演算法(一)
排序演算法 | 時間複雜度 | 是否基於比較 |
---|---|---|
冒泡、插入、選擇 | O(n²) | √ |
快排、歸併 | O(nlogn) | √ |
桶、計數、基數 | O(n) | × |
如何分析一個排序演算法
一、執行效率
- 最好情況、最壞情況、平均情況時間複雜度
- 時間複雜度的系統,常數、低階
- 比較次數和交換(移動)次數
二、記憶體消耗
記憶體消耗可以通過空間複雜度衡量。原地排序:特指空間複雜度為O(1)的排序演算法,不需要使用額外空間。
三、穩定性
穩定性:排序之前存在有兩個值相等的元素,排序之後,相等元素之間原有的相對位置不變
例子:訂單中有兩個屬性,金額和時間。現在希望想要按照金額由小到大進行排序,金額相同的再按照時間從早到晚排序。
解決辦法:利用排序演算法穩定性解決比較方便。首先按照時間從早到晚進行排序,排完序之後再使用穩定排序演算法對金額進行排序,因為穩定性,對於相同金額的訂單,排序前後的相對位置是不會發生改變,所以訂單就按照預期排好了。
排序演算法
排序演算法 | 是否為原地排序演算法 | 是否穩定 | 最好時間複雜度 | 平均時間複雜度 | 最壞時間複雜度 |
---|---|---|---|---|---|
氣泡排序 | 是,空間複雜度為O(1) | 穩定 | O(n) | O(n²) | O(n²) |
插入排序 | 是,空間複雜度為O(1) | 穩定 | O(n) | O(n²) | O(n²) |
選擇排序 | 是,空間複雜度為O(1) | 不穩定 | O(n²) | O(n²) | O(n²) |
一、氣泡排序
氣泡排序只會比較相鄰兩個元素進行比較,看是否滿足大小關係,不滿足則互換位置,每次冒泡會讓至少一個元素移動到它應該在的位置,重複n次。
移動元素次數等於逆序度
優化:當某次冒泡沒有資料交換時,就說明已經排好序了,不需要再繼續執行後續冒泡操作了。
// 氣泡排序,a表示陣列,n表示陣列大小 public void bubbleSort(int[] a, int n) { if (n <= 1) return; for (int i = 0; i < n; ++i) { // 提前退出冒泡迴圈的標誌位 boolean flag = false; for (int j = 0; j < n - i - 1; ++j) { if (a[j] > a[j+1]) { // 交換 int tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; flag = true; // 表示有資料交換 } } if (!flag) break; // 沒有資料交換,提前退出 } }
二、插入排序
思想:動態向有序集合中插入資料,並一直保持集合有序
插入排序描述:將陣列分為兩部分,分別是:已排序區間和未排序區間。初始已排序區間只有一個元素,即陣列的第一個元素,之後不斷從未排序區間中取出元素,並插入到已排序區間的合適位置,保證已排序區間一直有序。重複這個過程直到未排序區間中元素為空。
移動元素次數等於逆序度
// 插入排序,a表示陣列,n表示陣列大小
public void insertionSort(int[] a, int n) {
if (n <= 1) return;
for (int i = 1; i < n; ++i) {
int value = a[i];
int j = i - 1;
// 查詢插入的位置
for (; j >= 0; --j) {
if (a[j] > value) {
a[j+1] = a[j]; // 資料移動
} else {
break;
}
}
a[j+1] = value; // 插入資料
}
}
三、選擇排序
選擇排序思路:陣列也分為已排序區間和未排序區間,每次在未排序區間中從第一位元素開始找,找到最小元素,將其與未排序區間第一位元素交換位置,就變為了已排序區間的末尾。
// 選擇排序,a表示陣列,n表示陣列大小
public static void selectionSort(int[] a, int n) {
if (n <= 1) return;
for (int i = 0; i < n - 1; ++i) {
// 查詢最小值
int minIndex = i;
for (int j = i + 1; j < n; ++j) {
if (a[j] < a[minIndex]) {
minIndex = j;
}
}
// 交換
int tmp = a[i];
a[i] = a[minIndex];
a[minIndex] = tmp;
}
}
思考
氣泡排序和插入排序的時間空間複雜度,穩定性都相同,但為什麼插入排序更受歡迎?
氣泡排序中需要交換的元素個數,和選擇排序需要移動元素個數是固定相同的,即都為原始資料的逆序度。
但是氣泡排序資料交換要比插入排序資料移動複雜,氣泡排序需要3個賦值操作,而插入排序只需要1個
//氣泡排序中資料的交換操作:
if (a[j] > a[j+1]) { // 交換
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true;
}
//插入排序中資料的移動操作:
if (a[j] > value) {
a[j+1] = a[j]; // 資料移動
} else {
break;
}
假設將一個賦值語句耗時時間為單位時間,分別使用氣泡排序和選擇排序對同一個逆序度為K的陣列進行排序:
氣泡排序:需要K次交換操作,每次3個賦值語句,所以總耗時時間為3*K;
插入排序:需要K次移動操作,每次1個賦值語句,所以總耗時時間我K;
所以,雖然時間複雜度都為O(n²),但插入排序的效能更加極致優化。如果需要對插入排序進一步優化,可以瞭解下希爾排序